go语言基础(四):指针、面向对象(继承)、方法
1. 指针
- 注意:无法获取常量内存地址
- 指针定义的语法:
var 指针变量名 *数据类型
- 想要访问指针指向的数据,要使用解引用
- 有关指针的概念性的东西和C语言完全相同
func main() {
num := 10
var p *int = &num
fmt.Printf("%T\n", p) // *int
fmt.Printf("%d\n", *p) // 10
}
2.1 使用new开辟堆空间
语法:new(数据类型)
,返回对应数据类型的指针
func main() {
var p *int
// new(数据类型) 开辟数据类型对应的内存空间,并初始化,默认初始化值为0
p = new(int)
p1 := new([5]int)
fmt.Printf("%T\n", p1) // 数组指针:*[5]int
fmt.Println(*p) // 0
p = nil
}
go语言中不需要自己去释放new出来的空间,可以自己人为的将不再使用的指针置为空,有垃圾回收机制gc
- 垃圾回收机制gc
- 标记清除(三色标记):使用在超过32KB大小的内存
- 引用计数:小于32KB的使用引用计数方式回收,如果有引用,计数+1,如果没有使用,计数-1。当引用计数为0时释放该空间
2.2 指针作为函数参数
比较经典的一个栗子,通过函数传递交换两个变量的值
func test(a *int, b *int) {
*a, *b = *b, *a
}
func main() {
a := 10
b := 20
fmt.Println(a)
fmt.Println(b)
test(&a, &b)
fmt.Println(a)
fmt.Println(b)
}
2.3数组指针
数组指针在go语言中做了优化,可以当做数组名来使用,主要用处有两个:一是在访问数组元素时,二是在获取数组元素个数时
func main() {
arr := [5]int{1, 2, 3, 4, 5}
p := &arr // 数组指针 *[5]int 类型
fmt.Printf("%T\n", p)
// 通过指针操作数组
fmt.Println((*p)[0]) // 打印 1
for i:=0; i<len(p); i++ {
// go语言中对数组指针进行了优化
// 1.可以直接使用 指针名[index]访问数组元素
// 2.可以直接使用 len(指针名) 来获取数组元素个数
// fmt.Println((*p)[i]) 被优化
fmt.Println(p[i])
}
}
func main() {
// 在开辟堆空间时 可以将指针当做数组名一样来操作数组
var p *[5]int = new([5]int)
p[0] = 123
p[1] = 234
p[2] = 345
p[3] = 456
p[4] = 567
for i:=0; i<len(p); i++ {
fmt.Println(p[i])
}
}
注意:前提条件是数组指针和一个实体的数组建立了关系
- 数组作为函数参数是值传递,所以如果函数的外部需要访问的话需要用一个函数返回值进行传出
- 如果不使用函数的返回值传出的话,那么就可以使用函数指针,这样传递的进去的是一个地址
func BubbleSort(arr *[5]int) { // 传递的是数组指针
for i:=0; i<len(arr)-1; i++ {
for j:=0; j<len(arr)-1-i; j++ {
if arr[j] > arr[j+1] {
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
}
func main() {
arr := [5]int{5, 4, 3, 2, 1}
fmt.Println(arr)
BubbleSort(&arr) // 传递的是数组指针
fmt.Println(arr)
}
2.4 指针数组
语法:var 数组名 [元素个数]*数据类型
数组中的每个元素都是一个指针。
func main() {
a := [3]int{1, 2, 3}
b := [3]int{4, 5, 6}
c := [3]int{7, 8, 9}
// 数组指针数组
var arr [3]*[3]int = [3]*[3]int{&a, &b, &c}
for i:=0; i<len(arr); i++ {
fmt.Println(*arr[i])
}
// 打印元素的值
for i:=0; i<len(arr); i++ {
for j:=0; j<len(arr[i]); j++ {
fmt.Print(arr[i][j], " ")
}
fmt.Println()
}
}
2.5 结构体指针
语法:var 指针名 *结构体类型
type Student struct {
id int
name string
age int
}
func main() {
// 结构体指针变量
var p *Student = new(Student)
// go语言做了优化,结构体指针变量直接使用.就可以访问成员变量
// (*p).id = 1001
p.id = 1001
p.name = "alin"
p.age = 21
fmt.Println(*p)
// 如果直接打印结构体指针变量的话,结果前面会有一个&
fmt.Println(p)
}
2.6 切片指针
切片指针的类型:*[]数据类型
- 要注意在访问切片元素时不能直接.出来,要用
(*p)[index]
func main() {
slice := []int{1, 2, 3, 4, 5}
var p *[]int = &slice
fmt.Println(*p)
// 切片指针在获取切片中的元素时要注意与数组区分
// 在这里没有对它进行优化,必须要使用(*p)[index]
fmt.Println((*p)[0])
// fmt.Println(p[0]) // 报错
}
- 切片在传入函数时,为了不发生错误,使用切片指针传入
- 因为在切片扩充时,如果切片后面的内存被占用,那么将无法原地进行切片的扩充,就需要另寻空间,此时的slice变为了新的值,那么形参和实参则不相同
- 使用切片指针会解决这个问题
func test1(slice *[]int) {
*slice = append(*slice, 1,2,3,4,5)
}
func main() {
slice := []int{1, 2, 3, 4, 5}
test1(&slice)
fmt.Println(slice)
}
2.7 指针可以使用的运算符
-
==
和!=
可以在指针的比较中使用。 -
>
和<
不能用于指针的比较中。即指针不允许使用算数运算符计算,要和C语言区分,C语言中允许使用-
2.8 多级指针
func main() {
a := 10
var p *int = &a // 一级指针
var pp **int = &p // 二级指针
// pp = &(&a) // 错误,&不能连用,加括号也不行
fmt.Println(**pp)
}
2. 面向对象:继承
2.1 给对象绑定方法(描述对象的行为)
语法:
func (对象名 类名) 函数名(函数参数) 函数返回值 {
语句
}
- 这是在给func后面括号中的类的对象绑定方法,所有的这个类的对象都可以通过
对象名.函数名
使用这个方法
2.2 匿名字段
go语言中的继承:将一个结构体作为另外一个结构体的成员,使用匿名字段,也就是没有对象名的类名
- go语言对匿名字段进行了优化,通过
子类.父类的成员变量
可以访问继承过来的成员变量 - 如果不使用匿名字段,使用实名字段给继承的父类添加了名字后,不能再使用上述的优化
type Person struct {
id int
name string
sex string
}
type Student struct {
Person // 匿名字段实现继承
score int
}
type Teacher struct {
Person
subject string
}
func main() {
var stu Student
// 子类对象.父类.成员
stu.Person.name = "huahua"
stu.Person.sex = "女"
stu.Person.id = 1001
stu.score = 100
// go语言做了优化,可以直接使用子类对象.父类成员名进行访问
stu.name = "alin"
stu.id = 1002
stu.sex = "男"
fmt.Println(stu)
}
2.3 同名字段
就近原则
- 如果当同名字段比较多的时候,建议使用实名字段进行继承,因为需要多写一个类名
type Person1 struct {
id int // 8
name string //16
sex string //16
}
type Student1 struct {
Person1 // 匿名字段实现继承
name string //16
score int // 8
}
func main() {
stu := Student1{Person1{1001, "alin", "男"}, "alin666", 100}
fmt.Println(stu)
fmt.Println(stu.name) // 就近原则,打印:alin666
fmt.Println(stu.Person1.name) // 打印:alin
fmt.Println(unsafe.Sizeof(stu))
}
2.4 指针匿名字段
匿名字段继承时使用一个指针来表示基类
type Person2 struct {
name string
id int
age int
}
type Student2 struct {
*Person2
score int
}
func main() {
stu := Student2{&Person2{"alin", 1001, 21}, 100}
// 可以通过 对象名.父类成员 访问父类成员变量
// 不必写 *(stu.Person2).name
stu.name = "alin666"
fmt.Println(*(stu.Person2))
}
- 在使用指针匿名字段的时候,如果只创建了一个变量而没有对变量进行初始化的时候,不能使用
对象名.父类的成员变量
,因为父类的指针还是一个空指针,不能对空指针进行操作。解决办法:先对父类指针new一下再使用
type Person2 struct {
name string
id int
age int
}
type Student2 struct {
*Person2
score int
}
func main() {
var stu Student2
stu.name = "alin"
stu.id = 1001
stu.age = 21
stu.score = 100
fmt.Println(stu)
}
// 报错
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x206a]
修改后:
func main() {
var stu Student2
// 添加如下的语句
stu.Person2 = new(Person2)
stu.name = "alin"
stu.id = 1001
stu.age = 21
stu.score = 100
fmt.Println(stu)
}
2.5 多重继承
-
嵌套继承
type Human struct { name string sex string } type Person3 struct { Human age int } type Student3 struct { Person3 score int } func main() { stu := Student3{Person3{Human{"alin", "男"}, 21}, 100} stu.name = "alin666" // 可以直接使用对象名.成员变量进行成员变量的修改 fmt.Println(stu) }
-
继承多个类
type Human1 struct { name string sex string } type Person4 struct { age int addr string } type Student4 struct { Human1 Person4 score int } func main() { stu := Student4{Human1{"alin", "男"}, Person4{21, "TYUT"}, 100} stu.name = "alin666" fmt.Println(stu) }
- 尽量在程序中减少多重继承的使用,因为这样会增加程序的复杂度。可能会出现很多的同名字段,如果有很多同名字段的话,可以使用实名字段继承来避免错误的产生。
2.6 结构体的相互嵌套
结构体不允许相互嵌套,如果使用普通的匿名字段会报错。
如果打算使用的话:使用指针匿名字段。
- 同样的,如果说结构体想要嵌套自己,也要使用指针匿名字段
3. 方法
方法指的是类中的函数
语法:func (方法的接受者) 方法名(方法的参数列表) 返回值列表 {}
type INT int // 给int起别名为INT
// 因为绑定方法不能给普通内置类型绑定,所以给int起一个别名后可以当成创建一个对象来使用
func (a INT) add(b INT) {
sum := a+b
fmt.Println(sum)
}
func main() {
// 相当于创建两个int类型的对象
var a INT = 10
var b INT = 20
// 对象名.方法
a.add(b)
}
- 以上代码中要注意两个地方
- 使用type给内置数据类型起别名后,可以用来当做创建对象使用
- func绑定对象的时候,不能使用内置数据类型进行绑定,要使用一下type
3.1 类的方法
当调用类的方法的时候,函数参数的传递发生两次,第一次是将对象作为参数传递进函数,第二次是传递函数的参数列表。
type Student5 struct {
id int
name string
age int
}
func (stu Student5) EditInfo() {
stu.name = "alin666"
fmt.Println("方法内:", stu)
}
func main() {
stu := Student5{1001, "alin", 21}
stu.EditInfo() // 在此时发生了值传递,将stu这个对象也作为了一个函数参数传入了函数内部
fmt.Println("方法外:", stu)
}
// 结果
方法内: {1001 alin666 21}
方法外: {1001 alin 21}
以上代码中函数内外的stu是不同的两个变量,所以修改一方的值,另一方不会发生变化。
- 解决办法:绑定对象时使用类的指针
type Student5 struct {
id int
name string
age int
}
func (stu *Student5) EditInfo() {
// (*stu).name = "alin666" //优化为下面的代码
stu.name = "alin666"
fmt.Println("方法内:", *stu)
}
func main() {
stu := Student5{1001, "alin", 21}
// (&stu).EditInfo() // 传址进入函数参数,优化为下面的代码
stu.EditInfo()
fmt.Println("方法外:", stu)
}
// 结果
方法内: {1001 alin666 21}
方法外: {1001 alin666 21}
- 结论:如果在函数内部修改对象的属性在函数外部也要求修改的话,使用地址传递,加上一个*
3.2 方法继承
子类对象可以调用从父类继承的父类的方法
type Person6 struct {
id int
name string
age int
}
type Student6 struct {
Person6
score int
}
func (per *Person6) PrintInfo() {
fmt.Println("姓名:", per.name)
fmt.Println("id:", per.id)
fmt.Println("年龄:", per.age)
}
func (stu *Student6)test() {
// 通过子类对象调用从父类继承的方法
stu.PrintInfo()
fmt.Println("成绩:", stu.score)
}
func main() {
stu := Student6{Person6{1001, "alin", 21}, 100}
stu.test()
}
3.3 方法重写
子类和父类中有相同的方法名,叫方法的重写。
父类只能调父类自己的方法,子类可以通过子类对象.方法
调用子类的重写方法,也可以通过子类对象.父类名.父类方法
调用父类的方法。
type Person8 struct {
id int
name string
age int
}
type Student8 struct {
Person8
score int
}
func (per *Person8) PrintInfo() {
fmt.Println(*per)
}
func (stu *Student8) PrintInfo() {
fmt.Println(*stu)
}
func main() {
stu := Student8{Person8{1001, "alin", 21}, 100}
stu.PrintInfo() // 调用子类重写的方法
stu.Person8.PrintInfo() // 调用父类的方法
}
3.4 方法值和方法表达式
函数类型可以作为一个类型定义成一个函数类型的变量,可以用于函数调用和函数参数的传递
type Person9 struct {
id int
name string
age int
}
type Student9 struct {
Person9
score int
}
func test() {
fmt.Println("hello1")
}
func (stu *Student9) test() {
fmt.Println("hello2")
}
func main3() {
test()
var stu Student9
stu.test()
}
// 函数参数是函数类型变量
func test1(f func()) {
f()
fmt.Println("测试通过")
}
func main() {
var stu Student9
// 函数类型变量
var f func()
// 将函数赋值给函数类型变量
f = test
f()
// 将对象的方法赋值给函数类型变量
f = stu.test
// 通过函数类型变量调用对象方法
f() // 匿名接收者
// stu.f() // 报错,stu.只能用来访问对象内存在的成员变量或成员方法,其中没有f这个方法
test1(f)
}