首页 > 其他分享 >09.运算符重载

09.运算符重载

时间:2024-08-31 13:14:10浏览次数:11  
标签:函数 int 09 运算符 类型 重载 构造函数

9. 运算符重载

本小节将解释如何使C++的运算符能够处理类对象--一个称为运算符重载的过程。当运算符作用于类类型的运算对象时,可以通过运算符重载重新定义该运算符的含义。明智的使用运算符重载能令我们的程序更易于编写和阅读。

9.1 使用标准类库string中的重载操作符

#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1{"happy"};
    string s2{"birthday"};
    string s3;// creates an empty string

    cout<<"s1 is \"" <<s1<< "\"; s2 is \""<<s2
    <<"\"; s3 is \""<<s3<<'\"'
    <<"\n\nThe results of comparing s2 and s1:"<<boolalpha
    <<"\ns2 == s1 yields "<<(s2==s1)
    <<"\ns2 != s1 yields "<<(s2!=s1)
    <<"\ns2 > s1 yields "<<(s2>s1)
    <<"\ns2 < s1 yields "<<(s2<s1)
    <<"\ns2 >= s1 yields "<<(s2>=s1)
    <<"\ns2 <= s1 yields "<<(s2<=s1);

    cout<<"\n\ntesting s3.empty():\n";

    if(s3.empty()){
        cout<<"s3 is empty;assigning s1 to s3;\n";
        s3=s1;
        cout<<"s3 is \""<<s3<<"\"";
    }

    cout << "\n\ns1 += s2 yields s1 = ";
    s1 += s2; // test overloaded concatenation
    cout << s1;

    cout << "\n\ns1 += \" to you\" yields\n";
    s1 += " to you";//39行
    cout << "s1 = " << s1;

    // test string concatenation with a C++14 string-object literal
    cout << "\n\ns1 += \", have a great day!\" yields\n";
    s1 += ", have a great day!"s;
    cout << "s1 = " << s1 << "\n\n";

    cout << "The substring of s1 starting at location 0 for\n"
    <<"14 characters, s1.substr(0, 14), is:\n"
    <<s1.substr(0, 14)<<"\n\n";

    cout << "The substring of s1 starting at\n"
    <<"location 15, s1.substr(15), is:\n"<<s1.substr(15)<<"\n";

    // test copy constructor
    string s4{s1};
    cout << "\ns4 = " << s4 << "\n\n";

    // test overloaded copy assignment (=) operator with self-assignment
    cout << "assigning s4 to s4\n";
    s4=s4;
    cout << "s4 = " << s4;

    s1[0] = 'H';
    s1[6] = 'B';
    cout << "\n\ns1 after s1[0] = 'H' and s1[6] = 'B' is:\n"
    <<s1<<"\n\n";

    try{
        cout<<"attempting to assign 'd' to s1.at(100) yields:\n";
        s1.at(100)='d';
    }
    catch(out_of_range& x){
        cout<<"An exception occurred: " << x.what() << endl;
    }
}

运行结果如下:

s3使用默认的string构造函数创造空字符串。

used the stream manipulator boolalpha to set the output stream to display condition values as the strings "false" and "true".

string的成员函数,如果字符串为空,则返回true,反之,返回false。

string的成员函数substr,返回string对象的一部分。

函数 at() 在使用时会检查下标是否有效。如果给定的下标超出字符的长度范围,系统会抛出 out_of_range 异常。

1.如果at函数被non-const对象调用,此函数会返回一个可修改的左值,可以用在赋值操作符的左侧,可以赋给那个位置新的值。

2.如果at函数被const string对象调用,函数会返回一个不可修改的左值。只能获得那个值,而不能修改。

9.1.1 string类中的函数

c++ string 的常用库函数的用法_c++ string arg-CSDN博客

9.1.2 运算符与函数

1.Special Operators Usage with Objects (与对象一起用的运算符)

1.1. string类:使用“+”连接两个字符串

string s1("Hello"), s2("World!");

cout << s1 + s2 << endl;

1.2. array 与 vector类:使用[] 访问元素

array<char, 3> a{};

vector<char> v(3, 'a'); //'a', 'a', 'a'

a[0] v[1] = 'b';

1.3. path类:使用“/”连接路径元素

std::filesystem::path p**{};** 

p = p / "C:" / "Users" / "cyd";

2.The operator vs function (运算符与函数的异同)

2.1. 运算符可以看做是函数

2.2. 不同之处

2.2.1. 语法上有区别

3 * 2        //中缀式

* 3 2       //前缀式

multiply(3,2); //前缀式

3  2  *        //后缀式(RPN)

2.2.2. 不能自定义新的运算符 (只能重载)

3 ** 2    // C/C++中错误

pow(3, 2) // 3的平方

2.2.3. 函数可overload, override产生任何想要的结果,但运算符作用于内置类型的行为不能修改

multiply (3, 2)  // 可以返回1

3 * 2            // 结果必须是6

2.3. 函数式编程语言的观念

一切皆是函数

Haskell中可以定义新的运算符

9.2 运算符重载基础

9.2.1 运算重载不是自动的

必须编写运算符重载函数才可以执行所需的操作。运算符通过向往常一样编写non-static成员函数或者非成员函数来重载。只是函数名称以operator开头,后跟重载运算符的符号。例如,函数名为 operator+将用于重载运算符+,以便用于特定用户定义类型的对象。当运算符作为成员函数重载时,它们必须是non-static,因为它们必须在类的某个对象上被调用,并在该对象上进行操作。

9.2.2 不需要进行重载的操作符

为了在一个类的对象上使用一个运算符,必须为该类定义重载的运算符函数-除了三个例外:

1.赋值操作符(=),在大多数的类中用来数据成员的赋值。对于带指针成员的类,成员指派是危险的,因此我们将显式地重载此类类的指派运算符。

2.取地址操作符(&)

3.逗号操作符(,)

9.2.3 不能进行重载的运算符

sizeof运算符也不能重载。

运算符不可以重载

9.2.4 运算符重载的规则和限制

当你准备为自己的类重载运算符时,有几个规则和限制是你应该牢记的:

1.重载不会改变操作符的优先级

2.操作符的结合性不会因为重载而改变

3.An operator’s “arity” (that is, the number of operands an operator takes) cannot be changed

4.只有现存的操作符可以重载

5.不可以重载操作符以改变操作符对基本类型变量的作用,例如,不能让+表示两个整数相减。操作符重载仅仅工作在用户自定义的类的对象或者(用户自定义类型的对象和基本类型对象的混合)。运算符作用于C++内部提供的数据类型时,原来含义保持不变

6.像+和+=,必须单独重载

7.当重载( ),[ ],->或者任何赋值操作符时,操作符重载函数必须声明为类的成员。其他的情况,操作符重载函数可以是成员函数或者非成员函数。

8.不能创造新的运算符

9.2.5 运算符重载函数的格式

重载的运算符是具有特殊名字的函数:他们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包括返回类型、参数列表以及函数体。

1.运算符重载函数作为类的成员函数

函数类型 operator 重载运算符(形参列表){
    函数体;
}//形参个数=原操作数个数-1(后置++、--除外)

2.运算符重载函数作为类的友元函数

friend 函数类型 operator 重载运算符(形参列表){
    函数体;
}

3.运算符重载为非成员函数

函数的形参代表自左向右排列的各操作数。

重载为非成员函数时,参数个数=原操作数个数(后置++、--除外),至少应该有一个自定义类型的参数。

‍后置单目运算符++和--的重载函数,形参列表中要增加一个int,但不必写形参名。

如果在运算符的重载函数中需要操作某类对象的私有成员,可以将此函数声明为该类的友元

“函数类型”指出重载运算符的返回值类型,operator是定义运算符重载函数的关键词,“重载运算符”指出要重载的运算符名字,是C++中可重载的运算符,比如要重载加法运算符,这里直接写“+”即可,“形参表”指出重载运算符所需要的参数及其类型。

9.3 重载一元运算符

9.3.1 重载++和--运算符

“++”和“–”重载运算符也有前缀和后缀两种运算符重载形式,以“++”重载运算符为例,其语法格式如下:

函数类型 operator ++()
函数类型 operator ++(int)

例如下述代码:

#include<iostream>
using namespace std;

class MyClass2
{
public:
    MyClass2(int i){ n = i; }
    int operator ++(){ n++; return n; }
    int operator ++(int){ n += 2; return n; }
    void display()
    {
        cout << "n=" << n << endl;
    }
private:
    int n;
};

int main()
{
    MyClass2 A(5), B(5);
    A++;
    ++B;
    A.display();
    B.display();
    system("pause");
}

9.3.2 重载->运算符

“->”运算符是成员访问运算符,这种单目运算符只能被重载为成员函数,一般成员访问运算符的格式如下:

对象->成员

成员访问运算符“->”函数重载的一般形式为:

数据类型 类名::operator->();

例如下述代码:

#include <iostream>
using namespace std;

class PClass
{
    int n; double m;
public:
    PClass *operator->()
    {
        return this;
    }
    void setvalue(int n1, double m1)
    {
        n = n1; m = m1;
    }
    void disp()
    {
        cout<< "n=" << n << ",m=" << m<<endl;
    }
};

int main()
{
    PClass s;
    s->setvalue(10, 20.5);
    s->disp();
    s.setvalue(20, 89.8);
    s.disp();
}

上述程序中,重载“->”运算符的成员函数,该函数返回当前对象的指针。从而导致“s->disp();”和“s.disp();”两个语句都是正确的,实际上,前者通过调用重载“->”运算符成员函数转换成后者的格式。

9.4 重载二元操作符

二元操作符可以重载为含有一个形参的non-static成员函数或者重载为含两个形参(其中一个形参必须是类的对象或者类的对象的引用)的非成员函数。一个非成员操作符函数经常因为性能原因被声明为类的friend。

9.4.1 二元操作符重载函数(non static成员函数)

考虑使用<比较自行定义的属于string类的两个对象。当将二元运算符<重载为non-static成员函数,如果y和z是string类的对象,那么y<z会被编译器看作y.operator < (z),y通过this指针传递,z通过参数传递。例如下列代码,重载<运算符:

class String{
public:
    bool operator<(const String&) const
    ...
};

只有当二元操作符的左操作数是类的对象,二元操作符的重载函数才可以是类的成员函数。

若要将二元运算符函数声明为非静态成员函数,您必须用以下形式声明它:

ret-type operator op(arg)

其中,ret-type 是返回类型,op 是上表中列出的运算符之一,而 arg 必须是类的对象。

9.4.2 二元操作符重载函数(非成员函数)

若要将二元运算符函数声明为全局函数,您必须用以下形式声明它:

ret-type operator op(arg1,arg2)

其中,ret-type 和 op 是成员运算符函数,而 arg1 和 arg2 是自变量。 至少有一个参数必须是类的对象(或者是对象的引用)。

对二元运算符的返回类型没有限制;但是,大多数用户定义的二元运算符将返回类类型或对类类型的引用。

如果y和z都是类的对象或者对象的引用,编译器会看作operator<(y,z)

bool operator<(const String&, const String&);

9.5 重载二元流插入和提取操作符

#ifndef TEST_10_24_PHONENUMBER_H
#define TEST_10_24_PHONENUMBER_H

#include <iostream>
#include <string>

class PhoneNumber{
    friend std::ostream& operator<<(std::ostream&,const PhoneNumber&);
    friend std::istream& operator>>(std::istream&,PhoneNumber&);
private:
    std::string areaCode;
    std::string exchange;
    std::string line;
};
#endif //TEST_10_24_PHONENUMBER_H
//
// Created by 22364 on 2023/10/24.
//
#include <iomanip>
#include "PhoneNumber.h"
using namespace std;

ostream& operator<<(ostream& output,const PhoneNumber& number){
    output << "Area code: " << number.areaCode << "\nExchange: "
    <<number.exchange << "\nLine: " << number.line << "\n"
    <<"(" << number.areaCode << ") " << number.exchange << "-"
    <<number.line << "\n";
    return output;// enables cout << a << b << c;
}
// overloaded stream extraction operator; cannot be a member function
// if we would like to invoke it with cin >> somePhoneNumber;
istream& operator>>(istream& input, PhoneNumber& number){
    input.ignore();
    input >> setw(3) >> number.areaCode;
    input.ignore(2);
    input >> setw(3) >> number.exchange;
    input.ignore();
    input >> setw(4) >> number.line;
    return input;
}
#include <iostream>
#include "PhoneNumber.h"
using namespace std;

int main(){
    PhoneNumber phone;

    cout<<"Enter phone number in the form (555) 555-5555:"<<endl;

    // cin >> phone invokes operator>> by implicitly issuing
    // the non-member function call operator>>(cin, phone)
    cin>>phone;

    // cout << phone invokes operator<< by implicitly issuing
    // the non-member function call operator<<(cout, phone)
    cout<<phone<<endl;
}

在第二张图中,对于<<的重载函数的定义,当在mian函数中调用此函数后,引用形参input就变成了cin的alias,引用参数number就变成了phone的alias.由于两个函数都被声明为类的friend,所以可以访问类的private成员。流操作符setw限制了读入string的字符。When used with cin and strings, setw restricts the number of characters read to the number of characters specified by its argumen t (i.e., setw(3) allows three characters to be read).调用istream的成员函数ignore可以略过括号、空格和破折号。

dash characters---破折号

cin>>phone

相当于:

operator>>(cin,phone)
cout<<phone

相当于:

operator<<(cout,phone)

operator<<和operator>>函数都被声明为非成员friend函数,他们是非成员函数是因为PhoneNumber类的对象必须是操作符的右操作数。二元操作符的重载函数只有在左操作符是类的对象时才可以成为类的成员函数。Overloaded operator functions for binary operators can be member functions only when the left operand is an object of the class in which the function is a member.

成员函数重载:可以通过this指针访问本类的成员,可以少写一个参数,但是表达式左边的的第一个参数必须是类对象,通过该类对象来调用成员函数。即表达式左侧的左侧操作数就是对象本身。例如对于cout​<<classobject,classobject是用户自定义的类的对象,通过该类调用成员函数,即使用cout对象调用operator<<函数,这样的话,operator<<​需要是cout对象从属的ostream类的成员函数,但是ostream类属于C++的标准库类,不允许修改。因此,如果右操作数是用户自定义的类的对象,必须将<​<重载为非成员函数。

9.6 重载一元操作符

一元操作符可以重载为没有参数的non-static成员函数或者一个参数的非成员函数。此参数必须是对象或者对象的引用。成员函数必须是non-static 的才可以访问类的每个队形的non-static成员。

9.6.1 重载一元操作符函数作为成员函数

考虑重载一元操作符!,以此检验输入的string类的对象是否为空。这个函数会返回一个bool值。例如将操作符!重载函数重载为成员函数(没有参数),对于对象s,!s,编译器会将之认为s.operator!( ),操作数s是string类的对象,operator!为string类的成员函数。

class String{
public:
    bool operator!() const;
    ...
};
9.6.2一元操作符重载函数为非成员函数

还是考虑string类的对象s,s必须是类的对象或者对象的引用。!s会被编译器认为是:operator!(s)

bool operator!(const String&);

9.7 重载++和--

为了重载++和--操作符,编译器需要区分操作符是操作数的前缀还是后缀,因此重载函数必须有明显的特征以供编译器区分。

9.7.1 前缀递增操作符重载

考虑将整数1加到Date类的对象d1上。如果重载函数被定义为成员函数,编译器会生成成员函数调用:

d1.operator++();

此成员函数的原型为:

Date& operator++();//类名后的&必不可少

如果重载函数被声明为非成员函数,编译器会生成函数调用:

operator++(d1);

此函数的函数原型为:

Date& operator++(Date&);
9.7.2 后缀递增操作符重载

为了与前缀进行区分,对于d1++,编译器会生成成员函数调用:

d1.operator ++ (0);//0只用来区分前缀和后缀操作

函数原型为:

Date operator ++ (int);//在实现此函数时,不应该使用括号内的整型形参

参数0是一个虚职,为了让编译器区分前缀和后缀。如果后缀操作符重载函数为非成员函数,当编译器遇到d1++,会生成函数调用:

operator++(d1,0)

函数原型为:

Date operator++(Date&,int);

x++最终返回的是临时变量的值,而临时变量的值不能作为左值。x++是右值,编译器会先生成一份x值的临时复制,然后再对x递增,最后返回临时复制内容。++x不同,是对x递增后马上返回其自身,所以++x是左值。

The extra object that’s created by the postfix increment (or decrement) operator can result in a performance problem—especially when the operator is used in a loop. For this reason, you should prefer the overloaded prefix increment and decrement operators.

leap year---闰年

Wrap-around Error:当值的增量超过其类型的最大值时,就会发生环绕错误,进而导致“环绕”到非常小、负值或未定义的值。

helpIncrement函数用来防止出现上述错误。

9.8 动态内存管理(Dynamic Memory Management)和nullptr

可以控制内置或者用户自定义类型的对象或者数组的内存的分配和释放。这就叫做动态内存管理,使用操作符new和delete实现。可以使用new操作符在程序执行期间动态的分配精确数量的内存来存储对象或者数组。对象或内置数组在自由存储区(free store)创造,自由存储区是内存中分配给程序用来存储对象的区域。new和delete都是运算符,不是库函数,不需要单独添加头文件。

9.8.1 free store VS heap

C++ 自由存储区是否等价于堆? - melonstreet - 博客园 (cnblogs.com)

当我问你C++的内存布局时,你大概会回答:

“在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区”。

如果我接着问你自由存储区与堆有什么区别,你或许这样回答:

“malloc在堆上分配的内存块,使用free释放内存,而new所申请的内存则是在自由存储区上,使用delete来释放。”

这样听起来似乎也没错,但如果我接着问:

自由存储区与堆是两块不同的内存区域吗?它们有可能相同吗?

你可能就懵了。

事实上,我在网上看的很多博客,划分自由存储区与堆的分界线就是new/delete与malloc/free。然而,尽管C++标准没有要求,但很多编译器的new/delete都是以malloc/free为基础来实现的。那么请问:借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?

从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:

堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。

一旦内存分配完毕,就可以通过new返回的指针访问它。当不再需要此内存,就可以通过delete将它返回到自由存储区以释放内存。

9.8.2 使用new获取动态内存

new 运算符的用法如下:

Time* timePtr{new Time};
new <类型名>(初值);//申请一个变量的空间
new <类型名>[常量表达式];//申请数组

Time是任意类型名,timePtr是类型为Time*的指针。这样的语句会动态分配出一片大小为 sizeof(Time) 字节的内存空间,并且将该内存空间的起始地址赋值给 timePtr。如果要求分配的空间太大,操作系统找不到足够的内存来满足,那么动态内存分配就会失败,此时程序会拋出异常,或者返回空指针nullptr。

例如:

int* p;
p = new int;
*p = 5;

int *aPtr=new int[8];
int* arr1 = new int[5];
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };

第二行动态分配了一片 4 个字节大小的内存空间,而 p 指向这片空间。通过 p 可以读写该内存空间。

new运算符的初始化

int* buffer = new int{}; // 初始化为0
int* buffer = new int{0}; // 初始化为0
char* s=new char('a');
int* buffer = new int[512]{}; // 512个int都初始化为0
int* buffer = new int{5}; // 初始化为5

动态内存使用完毕以后,要使用delete运算符释放。销毁一个动态分配的对象并释放对象的内存,使用delete操作符:

delete <指针名>;//删除一个变量/对象
delete []<指针名>;//删除一个数组空间

不能删除未被new操作符分配的内存。这样做会导致未定义的行为。

删除一个动态分配的内存块后,一定不要再删除同一个块。防止这种情况的一种方法是立即将指针设置为nullptr。删除nullptr没有影响。

nullptr作为空指针

int* q=nullptr;

C++保证用删除数组的形式删除非数组的内存分配是安全的,如下例:

char* p=new char(32);
//可以使用下列两种方式删除
delete p;
delete [] p;

Operators new and delete can be overloaded, but this is beyond the scope of the book. If you do overload new, then you should overload delete in the same scope to avoid subtle dynamic memory management errors.

new和delete操作符也可以重载。如果重载了new,就要同时重载delete。

动态分配出来的内存没有变量名,只能通过指向它的指针来操作内存中的数据。

动态分配的内存生命周期和程序相同,程序退出时,如果没有释放,系统会自动回收。

当动态分配的内存不再需要时,不释放动态分配的内存会导致系统过早耗尽内存。这有时被称为 “内存泄漏”

当类的对象包含指向动态分配的内存的指针时,不提供拷贝构造函数以及重载赋值操作符是一个潜在的错误。

9.8.3 在堆中创建对象

在堆中创建对象,由程序员控制对象的生存期。

ClassName *pObject=new ClassName{};//用无参构造函数创建对象
ClassName *pObject=new ClassName{arguments};//用有参构造函数创建对象

9.9 拷贝构造函数

The null terminated strings are basically a sequence of characters, and the last element is one null character (denoted by ‘\0’).

对于动态分配内存的内置数组,range-based for不能正常工作

当类的对象包含指向动态分配的内存的指针时,不提供拷贝构造函数以及重载赋值操作符是一个潜在的错误。

9.9.1 拷贝构造函数

它是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。****拷贝构造函数就是函数名是当前类的名字,参数为当前类的另一个对象的函数,拷贝构造函数通常用于:

  • 通过使用另一个同类的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

声明拷贝构造函数:

Circle (Circle&);
Circle (const Circle&);

如果在类中没有定义拷贝构造函数,编译器会自行定义一个。编译器自定义生成的拷贝构造函数可以实现对应数据成员的复制。自定义的拷贝构造函数可以实现特殊的复制功能。 当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员。

The argument to a copy constructor should be a const reference to allow a const object to be copied.

如果不希望对象被复制构造,可以使用"=delete"指示编译器不生成默认拷贝构造函数。例如:

class Point{
public:
	Point(int xx=0,int yy=0){x=xx;y=yy;}//构造函数
	Point(const Point& p)=delete;
private:
	int x,y//私有数据
}

9.9.2 浅拷贝和深拷贝

1.浅拷贝:拷贝指针,而非指针指向的内容。简单的复制拷贝操作。

以下两种情况会出现浅拷贝:

1.创建新对象时,调用类的隐式/默认构造函数。

2.为已有对象赋值时,使用赋值运算符。

2.深拷贝:拷贝指针指向的内容。在堆区重新申请内存空间,进行拷贝操作。当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指对象进行复制。

‍而使用移动语义和移动构造函数,可以避免深拷贝的情况,节省资源。

如何深拷贝:

1.自行编写拷贝构造函数,不使用编译器隐式生成的拷贝构造函数

2.重载赋值运算符,不使用编译器隐式生成的赋值运算符函数

9.9.3 何时执行拷贝构造函数

1.用已有对象构造新对象的时候(通过使用另一个同类型的对象来初始化新创建的对象)

Student stu;//先定义一个对象
Student stu2(stu);//(1)会调用拷贝构造函数
Student stu3=stu ;//(2)会调用拷贝构造函数,这里和(1)是一样的,仅仅是写法不同而已

如果对象已经被创建,会调用赋值操作符而不是复制构造函数:

Student stu;

Student stu2;//在这里被创建

stu2 = stu;//这里调用赋值操作符重载(见后文)

代码示例:

#include <iostream>//cin cout
using namespace std;

class Student
{
public:
    //规则1:如果类的用户自己定义了构造函数
    // ,编译器就不会自动合成类的默认构造函数Student(void)
    //这样类就不存在默认构造函数了

    //类的用户自己定义了拷贝构造函数(拷贝构造函数也算是一个构造函数)
    //按照规则1,此时类不再拥有Student(void)这个函数
    Student(const Student& from)
    {
        cout << "copy constructor called." << endl;
    }

    //由于上面已经定义了一个构造函数,按照规则1,
    //Student stu;这条语句就会因为类不存在默认构造函数而编译报错
    //为了能够让我们可以写Student stu;这条语句来创建对象
    // ,我们在这里显示定义类的默认构造函数
    Student(void)
    {
        cout << "default constructor called." << endl;
    }
};

int main(int argv, char* argc[])
{
    cout << "flag1" << endl;//打印标记信息,用来查看函数执行的顺序
    Student stu;//先定义一个对象
    cout << "flag2" << endl;//打印标记信息,用来查看函数执行的顺序
    Student stu2(stu);//(1)会调用拷贝构造函数
    cout << "flag3" << endl;//打印标记信息,用来查看函数执行的顺序
    Student stu3 = stu;//(2)会调用拷贝构造函数,这里和(1)是一样的,仅仅是写法不同而已
    cout << "flag4" << endl;//打印标记信息,用来查看函数执行的顺序

    return 0;
}

2.给函数传递值类型参数的时候(复制对象把它作为参数传递给函数):调用函数时,将使用实参对象初始化形参对象,发生拷贝构造。

void test_function(Student s)
{//s在该函数被调用的时候创建,该函数执行完之后释放
    s.m_name = "李四";//修改s的名字
}
int main()
{
    Student stu("张三");
    test_function(stu);//(1)创建stu的副本,函数内使用副本
    cout<<stu.m_name;//还是输出“张三”,因为修改的是,函数内的副本
}

完整代码:

#include <iostream>//cin cout
#include <string>
using namespace std;

class Student
{
public:
    Student(const Student& from)//拷贝构造函数
    {
        cout << "copy constructor called." << endl;
    }
    //如果只提供上面的拷贝构造函数,编译器就不再生成默认构造函数
    //,会而导致类对象就不可以直接创建,所以还需要提供一个默认构造函数
    Student(void)
    {
        cout << "default constructor called." << endl;
    }
    Student(const string& name) :m_name(name)
    {
        cout << "string constructor called." << endl;
    }
    string m_name;//存放学生姓名
};
void test_function(Student s)
{//s在该函数被调用的时候创建,该函数执行完之后释放
    cout << "flag2" << endl;
    s.m_name = "李四";//修改s的名字
    cout << "flag3" << endl;
}//s释放的时刻
int main()
{
    cout << "flag0" << endl;
    Student stu("张三");
    cout << "flag1" << endl;
    test_function(stu);//(1)创建stu的副本,函数内使用副本
    cout << "flag4" << endl;
    cout << stu.m_name;//还是输出“张三”,因为修改的是,函数内的副本
}

#include <iostream>

using namespace std;

class Student {
public:
	Student();	// default构造函数
	Student(const Student& obj);	// 拷贝构造函数

	int getNumber();

private:
	int number;
};

// 定义默认构造函数
Student::Student(){
	this->number = 0;
    cout << "default constructor" << endl;
}

// 定义拷贝构造函数
Student::Student(const Student& obj) {
	this->number = obj.number;
    cout << "copy constructor" << endl;
}

int Student::getNumber() {
	return this->number;
}

// 输出某个对象的number
void showNumber(Student a) {
	cout << a.getNumber();
}

int main(){
	Student s;  // 调用默认构造函数
	showNumber(s);

	return 0;
}

你认为输出结果会是什么?首先肯定会输出一个"default constructor",因为s的默认构造函数输出了该字符串。那么,showNumber()会输出什么呢?正确的输出是:

default constructor
copy constructor
0

这就回到了我们开头的那句话,拷贝构造函数定义了一个对象如何以值传递给函数

在函数showNumber()中,参数a是以值传递的方式传入的。所以主函数中调用showNumber()时,对象s被复制到了形参a当中。这个复制操作是通过调用了拷贝构造函数完成的,所以调用了自身的拷贝构造函数,自然会输出一遍"copy constructor"。

相当于执行了这样一个操作

Student a(s);

等待showNumber()结束之后,再析构a这个对象。

这也就是为什么传递用户自定类型通常用引用传递,同时也解释了,为什么拷贝构造函数传参为什么是引用传参?

设想,如果拷贝构造函数这么写,会发生什么?

Student (const Student obj);

假设有个Student对象s。我值传递s进入了拷贝构造函数,那么按照之前所说,编译器会去申请一个空间给形参obj,同时调用自身的拷贝构造函数把s的数据成员的值传给obj。但是这个调用拷贝构造函数的过程,又是一个值传递,又需要开辟一个空间....

所以,只要以值传递的方式调用拷贝构造函数,就会申请空间,申请空间的过程中又会申请空间,又申请空间的过程又又申请空间。如此往复,会陷入无限套娃的死循环。

所以拷贝构造函数一定不能是值传递。

3.函数返回值类型对象的时候:函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生拷贝构造。

Student get_copy(void)
{
    Student s;
    return s;//这里以s为参数构造构造一个副本,并返回副本,这里发生了拷贝
}

完整代码:

#include <iostream>//cin cout
#include <string>
using namespace std;

class Student
{
public:
    Student(const Student& from)//拷贝构造函数
    {
        cout << "copy constructor called." << endl;
    }
    //如果只提供上面的拷贝构造函数,编译器就不再生成默认构造函数
    //,会而导致类对象就不可以直接创建,所以还需要提供一个默认构造函数
    Student(void)
    {
        cout << "default constructor called." << endl;
    }
};
Student test_function(void)
{//s在该函数被调用的时候创建,该函数执行完之后释放
    cout << "flag2" << endl;
    Student stu;
    cout << "flag3" << endl;
    return stu;
}//s释放的时刻
int main()
{
    cout << "flag1" << endl;
    test_function();//(1)创建stu的副本,函数内使用副本
    cout << "flag4" << endl;
}

9.9.4 句柄(handle)

句柄不能是常量,在程序设计中,句柄是一种特殊的智能指针,当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。

9.10 Operators as Member vs. Non-Member Functions

当操作符函数为成员函数时,左操作数必须是一个对象(或者是对象的引用)。如果左操作数必须是不同类的对象或者基础类型,则操作符函数必须声明为非成员函数。如果需要非成员函数访问类的private和protected成员,可以将非成员函数声明为类的friend。

9.11 类型转换

对象的类型定义了对象能够包含的数据和能够参与的运算,其中一种运算被大多数类型支持,就是将对象从一种给定的类型转换为另一种相关类型。

当程序的某处我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换,当给某种类型的对象强行赋了另一种类型的值时,会发生什么呢?

bool b=42;//b为真
int i=b;//i的值为1
i=3.14;//i的值为3
double pi=i;//pi的值为3.0
unsigned char c=-1;//假设char为8bite,c的值为255
signed char c2=256;//假设char占8bite,c2的值是未定义的

类型所能表示的值的范围决定了转换的过程:

1.当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。

2.当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0-255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。

3.当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

当在程序的某处使用了一种算术类型的值而其实所需的是另一种类型的值时,编译器同样会执行上诉的类型转换。例如,我们使用了一个非布尔值作为条件,那么它会被自动的转换成布尔值,这一做法和把非布尔值赋给布尔变量时的操作完全一样:

int i=42;
if(i)//if条件的值将为true
    i=0;

如果i的值为0,则条件的值为false;i的所有非零取值都将使条件为true。如果我们把一个布尔值用在算术表达式里,则它的取值非0即1,所以一般不宜在算术表达式里使用布尔值。

尽管我们不会故意给无符号对象赋一个负值,却可能写出这么做的代码。例如,当一个算术表达式中既有无符号数又有int值时,那个int值就会转换为无符号数。把int转换为无符号数的过程和把int直接赋给无符号变量一样:

unsigned u=10;
int i=-42;
std::cout<<i+i<<std::endl;//输出-84
std::cout<<u+i<<std::endl;//如果int占32位,输出4294967264

在第二个表达式,相加前首先把整数-42转换为无符号数。把负数转换为无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值。

9.12 再探类型转换

类型转换分为隐式类型转换和显式类型转换(强制类型转换)。

9.12.1 隐式类型转换

如果两种类型有关联,那么当程序需要其中一种类型的运算对象时,可以用另一种关联类型的对象或值来替代。即,如果两种类型可以互相转换(conversion),那么它们就是关联的。

例如,下列表达式的目的是将val初始化为6.

int val=3.783+3;//编译器可能会警告该运算损失了精度

加法的两个运算对象类型不同:3.783为double类型,3为int类型。C++不会直接将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值。上述的类型转换是自动进行的,被称为隐式转换(implicit cinversion)。算术类型之间的隐式转换被设计的尽可能避免损失精度。

何时发生隐式类型转换

1.在大多数表达式中,比int类型小的整数值首先提升为较大的整数类型

2.在条件中,非布尔值转换为布尔类型

3.初始化过程中,初始值转换为变量的类型;复制语句中,右侧运算对象转换成左侧运算对象的类型

4.如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型

5.函数调用时也会发生类型转换

9.12.2 算术转换

算术转换的含义是把一种算术类型转换成另外一种算术类型。

整型提升:负责把小整数类型转换成较大的整数类型。对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要他们所有可能的值都能存在int里,他们就会提升为int类型;否则,提升成unsigned int类型。

较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能够容纳原类型所有可能的值。

9.12.3 其他隐式类型转换

除了算术转换之外还有几种隐式类型转换:

数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针:

int ia[10];
int* p=ia;//ia转换成指向数组首元素的指针

当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof及typeid等运算符的运算对象时,上述转换不会发生。如果用一个引用来初始化数组,上诉转换也不会发生。

指针的转换:1.常量整数值0或者字面值nullptr能转换成任意指针类型

2.指向任意非常量的指针能转换成void*

3.指向任意对象的指针能转换成const void*

转换成布尔类型:存在一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果是false;否则转换结果是true。

char *cp=get_string();
if(cp)//如果指针cp不是0,条件为真
while(*cp)//如果*cp不是空字符,条件为真

转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。也就是说,如果T是一种类型,我们就能将指向T的指针或引用分别转换成指向const T的指针或引用。

int i;
const int &j=i;//非常量转换成const int的引用
const int *p=&i;//非常量的地址转换成const的地址
int &r=j,*q=p;//错误:不允许const转换成非常量

相反的转换不存在,因为它试图删掉底层const。

类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。如果同时提出多个转换请求,这些请求将被拒绝。

类类型转换的例子:在需要标准库string类型的地方使用c风格字符串;在条件部分读入istream

string s,t="a value";//字符串字面值转换成string类型
while(cin>>s);//while的条件部分把cin转换成布尔值

9.12.4 显式转换(强制类型转换)

在 C 语言中,我们大多数是用 (type_name) expression 这种方式来做强制类型转换,但是在 C++ 中,更推荐使用四个转换操作符来实现显式类型转换:

有时我们希望显式的将对象强制转换成另一种类型,例如,在下列代码中执行浮点数除法:

int i,j;
double slope=i/j;

就要使用某种方法将i和/或j显式的转换成double,这种方法称作强制类型转换(cast)

一个命名的强制类型转换的格式如下:

cast-name<type>(expression);

type是转换的目标类型,expression是要转换的值。如果type是引用类型,则结果是左值。cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。

Ⅰ.static_cast:

任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。

double slope=static_cast<double>(j)/i;

当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。对于编译器无法自动执行的类型转换也非常有用。

如果要在原生数据类型(基础数据类型)之间进行数据转换,应该使用static_cast关键字。

如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换

1.将一个基本类型转换为另一个基本类型,例如将整数转换为浮点数或将字符转换为整数。

int a = 42;
double b = static_cast<double>(a); // 将整数a转换为双精度浮点数b

2.指针类型之间的转换

将一个指针类型转换为另一个指针类型,尤其是在类层次结构中从基类指针转换为派生类指针。

进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的

class Base {};
class Derived : public Base {};

Base* base_ptr = new Derived();
Derived* derived_ptr = static_cast<Derived*>(base_ptr); // 将基类指针base_ptr转换为派生类指针derived_ptr

3.引用类型之间的转换

类似于指针类型之间的转换,可以将一个引用类型转换为另一个引用类型。在这种情况下,也应注意安全性。

Derived derived_obj;
Base& base_ref = derived_obj;
Derived& derived_ref = static_cast<Derived&>(base_ref); // 将基类引用base_ref转换为派生类引用derived_ref

static_cast在编译时执行类型转换,在进行指针或引用类型转换时,需要自己保证合法性。

如果想要运行时类型检查,可以使用dynamic_cast进行安全的向下类型转换。

4.把空指针转换成目标类型的空指针
5.把任何类型的表达式转换为void类型
注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。

Ⅱ.const_cast:

用法:

 const_cast <new_type> (expression)

expression必须是一个指针、引用或者指向对象类型成员的指针。

1.修改const对象

当需要修改const对象时,可以使用const_cast来删除const属性。

const int a = 42;
int* mutable_ptr = const_cast<int*>(&a); // 删除const属性,使得可以修改a的值
*mutable_ptr = 43; // 修改a的值

2.const对象调用非const成员函数

当需要使用const对象调用非const成员函数时,可以使用const_cast删除对象的const属性。

class MyClass {
public:
    void non_const_function() { /* ... */ }
};

const MyClass my_const_obj;
MyClass* mutable_obj_ptr = const_cast<MyClass*>(&my_const_obj); // 删除const属性,使得可以调用非const成员函数
mutable_obj_ptr->non_const_function(); // 调用非const成员函数

不过上述行为都不是很安全,可能导致未定义的行为,因此应谨慎使用。const_cast用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。

Ⅲ.reinterpret_cast

reinterpret_cast用于在不同类型之间进行低级别的转换。

expression必须是一个指针、引用、算术类型、函数指针或者成员指针。

首先从英文字面的意思理解,interpret是“解释,诠释”的意思,加上前缀“re”,就是“重新诠释”的意思;cast 在这里可以翻译成“转型”,它仅仅是重新解释底层比特(也就是对指针所指针的那片比特位换个类型做解释),而不进行任何类型检查。

reinterpret_cast主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型

因此,reinterpret_cast可能导致未定义的行为,应谨慎使用。

1.指针类型之间的转换

在某些情况下,需要在不同指针类型之间进行转换,如将一个int指针转换为char指针。

int a = 42;
int* int_ptr = &a;
char* char_ptr = reinterpret_cast<char*>(int_ptr); // 将int指针转换为char指针

Ⅳ.dynamic_cast

(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。

(2)不能用于内置的基本数据类型的强制转换。

(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表。 只有定义了虚函数的类才有虚函数表

(5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

向上转换,即为子类指针指向父类指针(一般不会出问题);向下转换,即将父类指针转化子类指针。

向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。dynamic_cast操作符则可以在运行期对可能产生问题的类型转换进行测试。

‍编码规范:

类型转换必须显式声明,永远不要依赖隐式类型转换。

9.13 隐式的类类型转换(转换构造函数)

上述类型转换介绍了C++在内置类型之间定义了几种自动转换规则。我们也可以为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数(converting constructor),此构造函数将实参类型的对象转换成类类型。

能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

例如:在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则。也就是说,在需要使用Sales_data的地方,我们可以使用string或者istream代替:

string null_book="9-999-99999-9";
//构造一个临时的Sales_data对象
//
item.combine(null_book);

只允许一步类类型转换:编译器只会自动的执行一步类型转换。

关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为explicit的。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。

//错误:只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。
explicit Sales_data::Sales_data(istream& is){
    read(is,*this);
}

explicit构造函数只能用于直接初始化:发生隐式转换的另一种情况是当我们执行拷贝形式的初始化时(使用=)。此时我们只能使用直接初始化而不能使用explicit构造函数:

Sales_data item1(null_book);//正确:直接初始化
Sales_data item2=null_book;//错误:不能将explicit构造函数用于拷贝形式的初始化过程

当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。而且编译器将不会在自动类型转换过程中使用该构造函数。

9.14 类型转换运算符

类型转换运算符可以完成类类型的类型转换。类型转换运算符(conversion operator)是类的一种特殊成员函数,负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下所示:

operator type() const;

type表示某种类型。类型转换运算符可以面向任意类型(除了void以外)进行定义,只要该类型能作为函数的返回类型。因此,不允许转换成数组或者函数类型,但是允许转换成指针(包括数组指针及函数指针)或者引用类型。

类型转换运算符既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数。类型转换运算符通常不应该改变待转换对象的内容,因此类型转换运算符一般声明为const。

class SmallInt{
public:
    SmallInt(int i=0):val(i)
     {
          if(i<0||i>255)
               throw std::out_of_range("bad smallint value");
      }
      operator int() const{return val;}
private:
    std::size_t val;
};

上述代码将算术类型的值转换成SmallInt对象,而类型转换运算符将SmallInt对象转换成int.

9.15 Overloading the Function Call Operator ()

赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员函数。

对于赋值运算符来说,我们知道一个c++类,程序员如果没有为其定义了赋值操作符重载函数,编译器也会隐式的定义,这样倘若再定义全局赋值运算符重载函数,将会发生二义性。即使编译器允许这样的定义手法,在调用的时候也编译不过:

函数调用运算符必须是成员函数

9.16 移动语义

C++11标准中提供了一种新的构造方法---移动构造。在某些情况下,我们没有必要复制对象---只需要移动它们。

C++11引入移动语义:源对象资源的控制权全部交给目标对象

当临时对象在被复制后,就不再被利用了。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制操作。

&&是右值引用,函数返回的临时变量是右值

标签:函数,int,09,运算符,类型,重载,构造函数
From: https://www.cnblogs.com/yyyylllll/p/18390161

相关文章

  • JS 扩展运算符(...)
    平时在对接服务端的数据时,后端返回的数据格式总不尽相同,因此前端总是需要自己再把数据加工处理成自己想要的格式最近在表格中渲染数据数据时就遇到了部分渲染不出来的情况,后来发现是对层数据,不能直接渲染的原因。举个例子,一个数组或一个对象里面包含了另一个对象,那在第一层的属......
  • 04.循环语句 & 逻辑运算符
    4.循环语句and逻辑运算符4.1for循环for循环的基本结构为:两个分号是必不可少的,用while表示for可以写为:如果在for循环中,第一部分被省略,C++会假定判断条件始终为真,循环一直进行。程序经常在循环体中显示控制变量值或在计算中使用它,但这种使用不是必需的。控制变量通常用于......
  • 03.控制语句、运算符及bool类型
    3.控制语句、运算符及bool类型3.1算法任何计算问题都可以通过按特定顺序执行一系列操作来解决。用以下方法解决问题的程序:1.要执行的操作2.这些操作的执行顺序就叫做算法。指定程序中语句(操作)执行的顺序称为程序控制。3.3伪代码(Pseudocode)使用伪代码,不必担心C++中的......
  • Luogu P10997 Partition 题解 [ 蓝 ] [ 轮廓 dp ]
    Partition:一道dp神题,用到了以轮廓线的轨迹来做dp的技巧,和敲砖块这题的状态设计有点相似。观察首先观察样例,发现整张图可以看作是被两条线分隔开的。同时每个颜色的四个方向上又存在一大堆奇怪的性质,很容易发现这两条线一条是从左上到右下的线,另一条是从右下到左上的线。暴......
  • 基于SSM的公交车客流自动调整系统的设计与实现 毕业设计-附源码03009
    摘要随着城市公共交通需求的日益增长,公交车客流量的自动调整成为提升公交服务质量和运营效率的关键。本文提出了一种基于SSM(Spring、SpringMVC、MyBatis)框架的公交车客流自动调整系统的设计与实现方案。该系统通过实时监测公交车客流数据,结合预设的规则和策略,自动调整公交......
  • P3320 [SDOI2015] 寻宝游戏 与 P10930 异象石 与 CF176E Archaeology
    思路:考虑按照dfn序将关键点的集合排序后为\(a_0,a_1,\cdots,a_k\),则答案为:\[\frac{\sum\limits_{i=0}^k\operatorname{dis}(a_i,a_{(i+1)\bmodk})}{2}\]简单证明一下:需要找出包含一些关键点的最小联通导出子图。则随便以一个关键点为根,对于子树内没有关键点的子树直接......
  • day59-graph theory-part09-8.30
    tasksfortoday:1.digkstra堆优化版47.参加科学大会2.bellman_ford算法94.城市间货物运输I---------------------------------------------------------------------------------1.dijkstra堆优化版Thisisanoptimizationforthevanilladijkstra,throughus......
  • 20240827_102109 python 字符流遍历得到每一行的数据
    需求python字符流遍历得到每一行的数据读取文件的内容每隔一秒钟,显示一行内容示例1示例2......
  • (159)时序收敛--->(09)时序收敛九
    1目录(a)FPGA简介(b)Verilog简介(c)时钟简介(d)时序收敛九(e)结束1FPGA简介(a)FPGA(FieldProgrammableGateArray)是在PAL(可编程阵列逻辑)、GAL(通用阵列逻辑)等可编程器件的基础上进一步发展的产物。它是作为专用集成电路(ASIC)领域中的一种半定制电路而出现的,既解决了定制电路的不足......
  • 代码随想录算法训练营,29日 | 704. 二分查找,27. 移除元素,977.有序数组的平方,209.长度最
    数组基础文档讲解︰代码随想录(programmercarl.com)1.连续空间、相同类型元素2.元素只能覆盖3.二维数组的地址连续吗(C++连续,Java不连续)704.二分查找题目链接:704.二分查找文档讲解︰代码随想录(programmercarl.com)视频讲解︰二分查找日期:2024-08-29思路:第一反应是想到二分查......