go语言基础(四):指针、面向对象(继承)、方法
1. 指针
注意:无法获取常量内存地址 指针定义的语法:var 指针变量名 *数据类型想要访问指针指向的数据,要使用解引用有关指针的概念性的东西和C语言完全相同
func main() {num := 10var p *int = &numfmt.Printf("%T\n", p) // *intfmt.Printf("%d\n", *p) // 10
}
2.1 使用new开辟堆空间
new(数据类型)
func main() {var p *int// new(数据类型) 开辟数据类型对应的内存空间,并初始化,默认初始化值为0p = new(int)p1 := new([5]int)fmt.Printf("%T\n", p1) // 数组指针:*[5]intfmt.Println(*p) // 0p = 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 := 10b := 20fmt.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]) // 打印 1for 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] = 123p[1] = 234p[2] = 345p[3] = 456p[4] = 567for 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 intname stringage int
}func main() {// 结构体指针变量var p *Student = new(Student)// go语言做了优化,结构体指针变量直接使用.就可以访问成员变量// (*p).id = 1001p.id = 1001p.name = "alin"p.age = 21fmt.Println(*p)// 如果直接打印结构体指针变量的话,结果前面会有一个&fmt.Println(p)
}
2.6 切片指针
*[]数据类型
要注意在访问切片元素时不能直接.出来,要用(*p)[index]
func main() {slice := []int{1, 2, 3, 4, 5}var p *[]int = &slicefmt.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 指针可以使用的运算符
==!= ><-
2.8 多级指针
func main() {a := 10var 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 intname stringsex string
}type Student struct {Person // 匿名字段实现继承score int
}type Teacher struct {Personsubject string
}func main() {var stu Student// 子类对象.父类.成员stu.Person.name = "huahua"stu.Person.sex = "女"stu.Person.id = 1001stu.score = 100// go语言做了优化,可以直接使用子类对象.父类成员名进行访问stu.name = "alin"stu.id = 1002stu.sex = "男"fmt.Println(stu)
}
2.3 同名字段
就近原则
如果当同名字段比较多的时候,建议使用实名字段进行继承,因为需要多写一个类名type Person1 struct {id int // 8name string //16sex string //16
}type Student1 struct {Person1 // 匿名字段实现继承name string //16score int // 8
}func main() {stu := Student1{Person1{1001, "alin", "男"}, "alin666", 100}fmt.Println(stu)fmt.Println(stu.name) // 就近原则,打印:alin666fmt.Println(stu.Person1.name) // 打印:alinfmt.Println(unsafe.Sizeof(stu))
}
2.4 指针匿名字段
匿名字段继承时使用一个指针来表示基类
type Person2 struct {name stringid intage int
}type Student2 struct {*Person2score int
}func main() {stu := Student2{&Person2{"alin", 1001, 21}, 100}// 可以通过 对象名.父类成员 访问父类成员变量// 不必写 *(stu.Person2).namestu.name = "alin666"fmt.Println(*(stu.Person2))
}
在使用指针匿名字段的时候,如果只创建了一个变量而没有对变量进行初始化的时候,不能使用对象名.父类的成员变量,因为父类的指针还是一个空指针,不能对空指针进行操作。解决办法:先对父类指针new一下再使用
type Person2 struct {name stringid intage int
}type Student2 struct {*Person2score int
}func main() {var stu Student2stu.name = "alin"stu.id = 1001stu.age = 21stu.score = 100fmt.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 = 1001stu.age = 21stu.score = 100fmt.Println(stu)
}
2.5 多重继承
嵌套继承
type Human struct {name stringsex string
}type Person3 struct {Humanage int
}type Student3 struct {Person3score int
}func main() {stu := Student3{Person3{Human{"alin", "男"}, 21}, 100}stu.name = "alin666" // 可以直接使用对象名.成员变量进行成员变量的修改fmt.Println(stu)}
继承多个类
type Human1 struct {name stringsex string
}type Person4 struct {age intaddr string
}type Student4 struct {Human1Person4score 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+bfmt.Println(sum)
}func main() {// 相当于创建两个int类型的对象var a INT = 10var b INT = 20// 对象名.方法a.add(b)
}
以上代码中要注意两个地方 使用type给内置数据类型起别名后,可以用来当做创建对象使用func绑定对象的时候,不能使用内置数据类型进行绑定,要使用一下type
3.1 类的方法
当调用类的方法的时候,函数参数的传递发生两次,第一次是将对象作为参数传递进函数,第二次是传递函数的参数列表。
type Student5 struct {id intname stringage 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 intname stringage 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 intname stringage int
}type Student6 struct {Person6score 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 intname stringage int
}type Student8 struct {Person8score 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 intname stringage int
}type Student9 struct {Person9score int
}func test() {fmt.Println("hello1")
}func (stu *Student9) test() {fmt.Println("hello2")
}func main3() {test()var stu Student9stu.test()
}// 函数参数是函数类型变量
func test1(f func()) {f()fmt.Println("测试通过")
}func main() {var stu Student9// 函数类型变量var f func()// 将函数赋值给函数类型变量f = testf()// 将对象的方法赋值给函数类型变量f = stu.test// 通过函数类型变量调用对象方法f() // 匿名接收者// stu.f() // 报错,stu.只能用来访问对象内存在的成员变量或成员方法,其中没有f这个方法test1(f)
}