Go vs C++ vs python 基本数据类型
二、基本数据类型
go
Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异。
整形
分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64 |
浮点型
float32和float64 |
布尔值 bool
布尔类型变量的默认值为false。 |
复数
complex64和complex128 |
字符串
s1 := "hello" |
byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如: |
指针类型(Pointer)
在 Go 中,指针类型用于存储变量的内存地址。与 C 和 C++ 不同,Go 指针不能进行偏移和运算,因此它们更安全且更易于使用。指针类型以 *
开头,用于指定指针类型的底层类型。例如,*int
表示指向整数类型的指针。在 Go 中,通过 &
运算符可以获取一个变量的内存地址,而通过 *
运算符可以获取指针所指向的变量的值,指针类型的零值为 nil
,表示指针不指向任何有效的内存地址。因此,在使用指针之前,应该先进行空指针检查
var x int = 42 |
Array(数组)
数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。下标是从0
开始,最后一个元素下标是:len-1
,访问越界(下标在合法范围之外),则触发访问越界,会panic。
// 定义一个长度为3元素类型为int的数组a 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变 |
数组的初始化
var testArray [3]int //数组会初始化为int类型的零值 |
数组的遍历
var a = [...]string{"北京", "上海", "深圳"} |
多维数组
多维数组只有第一层可以使用...
来让编译器推导数组长度。
二维数组 |
数组是值类型
数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。
- 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
[n]*T
表示指针数组,*[n]T
表示数组指针 。
切片(slice)
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片声明
切片是一个引用类型,它的内部结构包含地址
、长度
和容量
。切片一般用于快速地操作一块数据集合。
- 切片拥有自己的长度和容量,我们可以通过使用内置的
len()
函数求长度,使用内置的cap()
函数求切片的容量。 - 切片
长度=high-low
,容量等于得到的切片的底层数组的容量。0 <= low <= high <= len(a)
,则索引合法,否则就会索引越界(out of range) - 要检查切片是否为空,请始终使用
len(s) == 0
来判断,而不应该使用s == nil
来判断。 - 切片之间是不能比较的,我们不能使用
==
操作符来判断两个切片是否含有全部相等元素
var name []T |
使用make()函数构造切片
make([]T, size, cap) |
- T:切片的元素类型
- size:切片中元素的数量
- cap:切片的容量
多维切片
四维数组 |
切片操作
切片遍历 |
扩容
- 首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
- 否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
- 否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
map
map声明
map[KeyType]ValueType |
- KeyType:表示键的类型。
- ValueType:表示键对应的值的类型。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap]) |
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
scoreMap := make(map[string]int, 8) |
判断某个键是否存在
value, ok := map[key] |
map操作
遍历 |
结构体Struct
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct
。 也就是我们可以通过struct
来定义自己的类型了。
Go语言中通过struct
来实现面向对象。
自定义类型
//将MyInt定义为int类型 |
type 类型名 struct { |
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
var 结构体实例 结构体类型 |
创建指针类型结构体
我们还可以通过使用new
关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person) |
从打印的结果中我们可以看出p2
是一个结构体指针,需要注意的是在Go语言中支持对结构体指针直接使用.
来访问结构体的成员
取结构体的地址实例化
使用&
对结构体进行取地址操作相当于对该结构体类型进行了一次new
实例化操作。
p3 := &person{} |
p3.name = "七米"
其实在底层是(*p3).name = "七米"
,这是Go语言帮我们实现的语法糖。
结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。结构体占用一块连续的内存.* 空结构体是不占用空间的。
|
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person
的构造函数。 因为struct
是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
func newPerson(name, city string, age int8) *person { |
方法和接收者
Go语言中的方法(Method)
是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者 self
。
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { |
其中,
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self
、this
之类的命名。例如,Person
类型的接收者变量应该命名为p
,Connector
类型的接收者变量应该命名为c
等。 - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
//Person 结构体 |
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
//Address 地址结构体 |
结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物 |
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问).
结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。
//JSON序列化:结构体-->JSON格式的字符串 |
结构体标签(Tag)
Tag
是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag
在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"` |
结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。
//Student 学生 |
结构体和方法补充知识点
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意.
正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值。
函数Func
Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”。
Go语言中定义函数使用func
关键字,具体格式如下:
func 函数名(参数)(返回值){ |
其中:
- 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
- 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。 - 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。 - 函数体:实现指定功能的代码块。
- 函数的参数和返回值都是可选的
- 函数的参数中如果相邻变量的类型相同,则可以省略类型
可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。
注意:可变参数通常要作为函数的最后一个参数。
返回值
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()
将所有返回值包裹起来。
返回值命名 函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return
关键字返回。
当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
return nil // 没必要返回[]int{} |
函数进阶
局部变量和全局变量重名,优先访问局部变量。
函数类型与变量
定义函数类型
我们可以使用type
关键字来定义一个函数类型,具体格式如下:
type calculation func(int, int) int |
上面语句定义了一个calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x, y int) int { |
函数作为参数
函数可以作为参数:
func add(x, y int) int { |
函数作为返回值
函数也可以作为返回值:
func do(s string) (func(int, int) int, error) { |
匿名函数和闭包
匿名函数多用于实现回调函数和闭包。
函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数)(返回值){ |
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
func adder() func(int) int { |
变量f
是一个函数并且它引用了其外部作用域中的x
变量,此时f
就是一个闭包。 在f
的生命周期内,变量x
也一直有效。
闭包进阶示例2:
func makeSuffixFunc(suffix string) func(string) string { |
闭包进阶示例3:
func calc(base int) (func(int) int, func(int) int) { |
闭包其实并不复杂,只要牢记闭包=函数+引用环境
。
defer语句
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
由于defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
func calc(index string, a, b int) int { |
内置函数介绍
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
接口interface
接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节
在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。
接口类型
type 接口类型名 interface{ |
- 接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er
,如有写操作的接口叫Writer
,有关闭操作的接口叫closer
等。接口名最好要能突出该接口的类型含义。 - 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
实现接口的条件
接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。
我们定义的Singer
接口类型,它包含一个Sing
方法。
// Singer 接口 |
我们有一个Bird
结构体类型如下。
type Bird struct {} |
因为Singer
接口只包含一个Sing
方法,所以只需要给Bird
结构体添加一个Sing
方法就可以满足Singer
接口的要求。
// Sing Bird类型的Sing方法 |
这样就称为Bird
实现了Singer
接口。
只要实现了Say()
方法都能当成Sayer
类型的变量来处理
type Sayer interface { |
Go语言中为了解决类似上面的问题引入了接口的概念,接口类型区别于我们之前章节中介绍的那些具体类型,让我们专注于该类型提供的方法,而不是类型本身。使用接口类型通常能够让我们写出更加通用和灵活的代码
面向接口编程
只要一个类型实现了接口中规定的所有方法,那么它就实现了这个接口。
我们可以将具体的支付方式抽象为一个名为Payer
的接口类型,即任何实现了Pay
方法的都可以称为Payer
类型
// Payer 包含支付方法的接口类型 |
接口类型变量
var x Sayer // 声明一个Sayer类型的变量x |
值接收者和指针接收者
// Mover 定义一个接口类型 |
类型与接口的关系
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。例如狗不仅可以叫,还可以动。我们完全可以分别定义Sayer
接口和Mover
接口,具体代码示例如下
// Sayer 接口 |
一个接口的所有方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机 |
接口组合
接口与接口之间可以通过互相嵌套形成新的接口类型,例如Go标准库io
源码中就有很多接口之间互相组合的示例。
// src/io/io.go |
对于这种由多个接口类型组合形成的新接口类型,同样只需要实现新接口类型中规定的所有方法就算实现了该接口类型。
接口也可以作为结构体的一个字段,我们来看一段Go标准库sort
源码中的示例。
// src/sort/sort.go |
在这个示例中还有一个需要注意的地方是reverse
结构体本身是不可导出的(结构体类型名称首字母小写),sort.go
中通过定义一个可导出的Reverse
函数来让使用者创建reverse
结构体实例。
func Reverse(data Interface) Interface { |
这样做的目的是保证得到的reverse
结构体中的Interface
属性一定不为nil
,否者r.Interface.Less(j, i)
就会出现空指针panic。
空接口
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
package main |
通常我们在使用空接口类型时不必使用type
关键字声明,可以像下面的代码一样直接使用interface{}
。
var x interface{} // 声明一个空接口类型变量x |
- 空接口作为函数的参数
func show(a interface{})
- 空接口作为map的值
var studentInfo = make(map[string]interface{})
接口值
由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体值之外,还需要记录这个值属于的类型。也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型
和动态值
类型type | nil |
---|---|
值 value | nil |
m = &Dog{Name: "旺财"} type = *dog value = 旺财 |
类型断言
我们可以借助标准库fmt
包的格式化打印获取到接口值的动态类型 ,而fmt
包内部其实是使用反射的机制在程序运行时获取到动态类型的名称
var m Mover |
Error 接口
Go 语言中把错误当成一种特殊的值来处理,不支持其他语言中使用try/catch
捕获异常的方式。
type error interface { |
error 是一个接口类型,默认零值为nil
。所以我们通常将调用函数返回的错误与nil
进行比较,以此来判断函数是否返回错误.
创建错误
errors.New("无效的id") |
错误结构体类型
此外我们还可以自己定义结构体类型,实现``error`接口。
// OpError 自定义结构体类型 |
补充
new和make
使用 new
和 make
可以创建新的对象或数据结构
new
函数用于分配一块新的内存,并将其初始化为零值,返回一个指向这块内存的指针。可以用于任何数据类型,但是并没有给这块内存赋值,因此在使用前需要进行赋值。
// 声明一个指向 int 类型的指针 |
make
函数则用于分配并初始化一个引用类型的对象(如 slice
、map
、channel
)。返回的是一个该类型的对象而非指针,因为这些对象本身就是引用类型,即指向某个底层数据结构的指针。make
函数会为这些对象分配内存,初始化其内部字段,最后返回该对象。
// 声明一个长度为 5 的 int 类型切片,初始值为 0 |
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
python
在Python中常见的数据类型有以下8个类型,分别是:int,整数类型(整形)、float,浮点类型(浮点型)、bool,布尔类型、str,字符串类型、list,列表类型、tuple,元组类型、dict,字典类型、set,集合类型
1 int |
类类型
class MyClass: |
c++ 类型
typedef 声明
typedef type newname; |
例如,下面的语句会告诉编译器,feet 是 int 的另一个名称:
typedef int feet; |
现在,下面的声明是完全合法的,它创建了一个整型变量 distance:
feet distance; |
整型
int: 整数类型,通常为 32 位,可表示范围为 -2^31 到 2^31-1 |
浮点型
float: 单精度浮点型,通常为 32 位,可表示范围为 1.17549e-38 到 3.40282e+38,精度为 6 位小数 |
字符型
char: 字符类型,通常为 8 位,可表示 ASCII 码的字符,例如 'A'、'B'、'C' 等 |
布尔型
bool: 布尔类型,通常为 1 位,可表示 true 或 false |
复数
在 C++ 标准库中,复数类型是通过 std::complex 实现的,它定义在 <complex> 头文件中。std::complex 是一个模板类,它接受一个模板参数表示元素类型,可以是 float、double、long double 等 |
枚举类型(enum)
枚举类型是一种用户自定义的类型,用于定义一些有限的命名值。例如,我们可以使用枚举类型定义一些颜色:
enum Color { |
指针类型(pointer)
指针类型是一种保存了内存地址的变量类型。指针变量通常用于动态内存分配、函数调用等方面。例如,下面的代码中,我们定义了一个指针变量 p
,并将它指向一个整型变量 x
的地址:
int x = 10; |
引用(reference)
在 C++ 中,引用是一种轻量级的指针,它提供了访问变量的另一种方式,它是某个变量的别名。引用通常用于函数参数、返回值和赋值。引用的语法使用 & 符号。引用和指针类似,它们都提供了对变量的间接访问。但是,引用比指针更加安全,因为它们不会出现空指针的情况。在定义引用时必须初始化它,否则会出现编译错误。另外,引用一旦初始化后,就不能再指向其他变量,因此,引用可以被视为常量指针。
int a = 5; |
数组类型(array)
数组类型用于保存一组相同类型的数据,可以用下标访问数组中的元素。例如,下面的代码中,我们定义了一个数组 a
,包含了三个整型元素:
int a[3] = {1, 2, 3}; |
结构体类型(struct)
结构体类型可以用于组合多个不同类型的变量,形成一个新的类型。例如,下面的代码中,我们定义了一个结构体 Person
,包含了两个成员变量 name
和 age
:
struct Person { |
共用体类型(union)
共用体类型可以让多个不同的变量共用一段内存空间,用于节省内存。例如,下面的代码中,我们定义了一个共用体 Number
,可以表示一个整型数、一个浮点数或一个字符:
union Number { |
类类型(class)
在C++中,类是一种用户自定义的数据类型,可以包含数据成员、成员函数等元素。使用class关键字定义类
类是一种用户自定义的数据类型,它可以封装数据和方法。类定义了一组相关的数据和方法,它们通常是一些有意义的操作的集合。C++中的类可以看作是一种数据类型的定义方式,类的实例化(对象)是具体的这种数据类型的实现.
类是面向对象编程(OOP)的基础,其中面向对象的思想主要体现在封装、继承和多态性方面。类可以使用访问修饰符(public、protected、private)来限制成员变量和成员函数的访问权限。类还可以包含构造函数、析构函数、静态成员、常量成员函数等特殊成员函数。
class Person { |
继承权限
Shape |
模板类型
模板类型(template)是 C++ 中非常重要的一种数据类型,它可以用来定义通用的数据类型或函数。模板类型分为类模板和函数模板两种。
类模板
类模板可以用来定义通用的类,例如标准库中的容器类模板 std::vector 和 std::map,它们可以用来存储任何类型的数据,类模板是用来定义类的蓝图,其中某些成员的类型不确定,而是用类型参数来表示。类型参数可以在使用类模板时指定,从而让编译器根据指定的类型生成对应的类代码如下所示:
template<typename T> |
函数模板
函数模板可以用来定义通用的函数,例如标准库中的算法函数 std::sort 和 std::find,它们可以用来操作任何类型的数据,函数模板则是用来定义函数的蓝图,其中某些参数或返回值的类型不确定,而是用类型参数来表示,如下所示
template<typename T> |
类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。
C++ 中有四种类型转换:静态转换、动态转换、常量转换和重新解释转换。
静态转换(Static Cast)
静态转换是将一种数据类型的值强制转换为另一种数据类型的值。
静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。
静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。
int i = 10; |
动态转换(Dynamic Cast)
动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。
class Base {}; |
常量转换(Const Cast)
常量转换用于将 const 类型的对象转换为非 const 类型的对象。
常量转换只能用于转换掉 const 属性,不能改变对象的类型。
const int i = 10; |
重新解释转换(Reinterpret Cast)
重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。
重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。
int i = 10; |
补充
- 多态性:C++ 支持静态多态和动态多态,静态多态通过函数重载和运算符重载实现,动态多态通过虚函数实现。
- 抽象类:C++ 中可以定义纯虚函数,一个类如果包含纯虚函数,该类就是抽象类,抽象类不能实例化对象,只能被其他类继承。
- 友元函数:C++ 中的友元函数可以访问类的私有成员,但不是类的成员函数,友元函数可以定义在类内或类外。
- 内联函数:C++ 中的内联函数在函数调用处直接展开,减少函数调用的开销,可以在函数前加 inline 关键字将其声明为内联函数。
- 类模板:C++ 中可以定义类模板,用来创建具有不同数据类型的类,类模板可以具有成员函数和成员变量。
- 构造函数和析构函数:C++ 中的构造函数用来初始化类的对象,析构函数用来清理对象所占用的资源,构造函数和析构函数都是特殊的成员函数,一个类可以有多个构造函数,但只能有一个析构函数。
- 拷贝构造函数和移动构造函数:C++ 中的拷贝构造函数用来复制一个对象到另一个对象,移动构造函数用来移动一个对象到另一个对象,移动构造函数可以更高效地将对象转移,避免了不必要的复制操作。