GO语言基础语法--------指针篇
前言:在这篇文章中我们将重点关注Go语言指针的运用和GO语言和C/C++语言中指针的区别,这将会包括内存管理,空指针处理等内容。
Go语言和C/C++语言在指针的概念和用法上有一些区别。接下来我将解释这些差异并给出一些例子:
1. 内存管理:
在C/C++中,指针提供了直接的内存访问和管理,允许进行指针算术和手动内存分配/释放(在C语言中你必须手动分配和释放内存)。这让C/C++指针的使用非常灵活,但大大增加了出现内存错误的风险。最重要的是这些在内存段出现的错误非常难处理,会花费程序员大量的时间在解决bug上。 在Go语言中,指针仍然存在,但是它使用了垃圾回收(Garbage Collection)机制来自动管理内存。Go语言的垃圾回收器会自动跟踪和释放不再使用的内存,从而避免了许多与手动内存管理相关的问题。这同时也解决了很多内存安全的问题,GO语言几乎是内存安全的,最重要的是大部分的错误你可以在编译时就发现而不是运行时,这大大节省了Debug的时间。下面我们会给出一些对比的例子:
1.使用new创建数组:
go
func main() {
// 创建两个指向包含1个int类型元素的数组的指针
var arrPtr1 *[1]int = new([1]int)
arrPtr2 := new([1]int);
// 使用指针访问和修改数组元素
(*arrPtr1)[0] = 10
(*arrPtr2)[0] = 15
// 打印数组元素
fmt.Println("Array elements:", *arrPtr1)
fmt.Printf("Array elements: %d", (*arrPtr2)[0])
}
编译运行后的返回结果:
javascript
Array elements: [10]
Array elements: 15
在上面这个例子中我们用两种不同的方法初始化了一个大小为一个int的数组,并返回它们的值。可以看出我们在GO语言中不需要手动分配内存,也不需要管内存释放。另外我们也可以发现在Go语言中创建的指针是数组指针而不是指向数组首元素地址的指针这和C语言是有明显区别。这么做的好处就是指针越界时就可以及时发现,不会等到运行时才能发现问题。
一个典型的C语言在堆上分配数组的例子:
c
int main(){
int *Array = (int*)malloc(sizeof(int) * 10);
for (int i = 0; i< 10; i++){
Array[i] = i;
}
for(int i = 0; i< 10; i++){
printf("%d", *(Array+i));
}
free(Array);
return 0;
}
不难发现我们在malloc时需要手动确定内存大小,并在使用完之后需要手动释放内存,Go语言中这些问题得到了很好的解决。
2.使用new创建切片和Map:
切片:
go
func main() {
arrPtr := new([]int)
*arrPtr = append(*arrPtr, 10)
*arrPtr = append(*arrPtr, 15)
fmt.Println("Slice elements:", *arrPtr)
}
运行后的返回结果:
ini
Slice elements: [10 15]
在上述例子中我们创建了一个长度为0的切片指针并增加了两个元素并用append给它增加了两个元素。需要注意的是append函数总是返回一个新的切片,这个新的切片可能指向了一个新的底层数组(如果原有的底层数组容量不够),或者是原切片的引用(如果原有的底层数组容量足够),所以要注意使用append时一定要把它赋值回原切片。
一个错误的Map使用的例子:
go
func main() {
// 使用new函数创建一个指向未初始化的map的指针
mPtr := new(map[string]int)
// 添加键值对(注意:这里需要先初始化map,否则会导致运行时错误)
(*mPtr)["apple"] = 1
}
在这个例子中,我们使用new函数创建一个指向未初始化的Map的指针,然后尝试直接通过指针添加键值对。这会导致运行时错误。使用new函数创建的指针指向的是一个未初始化的Map。这意味着该Map还没有被分配任何内存,也没有进行初始化。所以此时向map中添加键值对会导致运行时错误。
正确的Map的例子:
go
func main() {
// 使用new函数创建一个指向未初始化的map的指针
mPtr := new(map[string]int)
// 将指针转换为map类型
m := *mPtr
// 添加键值对(注意:这里需要先初始化map,否则会导致运行时错误)
m = make(map[string]int)
m["apple"] = 1
// 打印Map
fmt.Println("Map:", m)
}
运行后的返回结果:
arduino
Map: map[apple:1]
在使用make给指针初始化之后Map成功运行。
小结:
我们可以看出使用new来创建Map或者切片并不方便,尤其是Map不光需要反复解指针还需要担心初始化的问题。所以一般不推荐使用new来创建切片或者Map。
3.使用make创建切片和Map:
上文中我们发现用new创建切片或者Map并不方便,这是因为GO语言给我们提供了一种更好的方式那就是make。使用make来初始化Map和切片也是GO语言推荐的一种方式。
go
func main() {
// 使用make函数创建一个空的字符串到整数的Map,并添加一个键值对
m := make(map[string]int)
m["apple"] = 1
// 使用make函数创建一个包含1个int类型元素的切片,并添加一个元素
slice := make([]int, 1)
slice[0] = 10
// 打印切片元素和Map
fmt.Println("Slice elements:", slice)
fmt.Println("Map:", m)
}
在这个例子中,我们使用make函数分别创建了一个Map和一个切片,并对它们进行了添加和修改操作。由于make函数会初始化Map和切片,它们可以直接使用而不会导致运行时错误。使用make函数来创建切片、映射和通道,它会分配内存并初始化最后返回一个引用类型,所以使用make创建的切片或者Map可以直接用而不用解地址。
4.使用new创建结构体:
使用new创建结构体是GO语言中new最典型的用法了。下面我会给出一个使用new创建结构体的例子,并将它和C语言的进对比。
go
type Person struct {
Name string
Age int
Height float64
}
func main() {
// 使用new关键字创建一个Person类型的结构体指针
personPtr := new(Person)
// 使用指针访问和修改结构体字段
(*personPtr).Name = "John"
(*personPtr).Age = 30
(*personPtr).Height = 175.5
// 打印结构体字段
fmt.Println("Name:", (*personPtr).Name)
fmt.Println("Age:", (*personPtr).Age)
fmt.Println("Height:", (*personPtr).Height)
}
在这个例子中,我们定义了一个名为Person的结构体类型,包含了三个字段Name、Age和Height。然后,我们使用new关键字创建了一个指向Person类型的新结构体指针,并通过指针访问和修改结构体的字段。需要注意的是,虽然我们可以使用(*personPtr).Name这样的语法来访问结构体字段,但在实际编程中,更常用的做法是使用隐式指针解引用的方式。
go
// 使用隐式指针解引用访问和修改结构体字段
personPtr.Name = "John"
personPtr.Age = 30
personPtr.Height = 175.5
// 打印结构体字段
fmt.Println("Name:", personPtr.Name)
fmt.Println("Age:", personPtr.Age)
fmt.Println("Height:", personPtr.Height)
这么写也是正确的这是因为GO语言具有隐式指针解引用的特点。我们再来看一下C语言的结构体。
c
struct Person {
char name[50];
int age;
float height;
};
int main() {
// 使用malloc函数为Person类型的结构体分配内存
struct Person *personPtr = (struct Person *)malloc(sizeof(struct Person));
// 对结构体字段进行赋值
strcpy(personPtr->name, "John");
(*personPtr).age = 30;
personPtr->height = 175.5;
// 打印结构体字段
printf("Name: %s\n", personPtr->name);
printf("Age: %d\n", personPtr->age);
printf("Height: %.2f\n", personPtr->height);
// 使用完结构体后,记得释放动态分配的内存
free(personPtr);
return 0;
}
我们可以看到在这段代码中我们使用(*personPtr).age这样解引用的方式对结构体进行赋值也可以使用personPtr->height这样的方式直接给结构体赋值,需要注意的是C语言并不会隐式指针解引用,所以如果直接对结构体指针进行类似personPtr.age = 30这样的操作是错误的。
2. 空指针(nil pointer):
在C/C++中,指针可以具有空值,即指向空地址(null)。这样的指针在解引用时会导致程序崩溃或未定义行为。在Go语言中,指针也可以是nil,但Go语言中的空指针解引用会导致运行时错误而不是崩溃。这样的设计使得在Go代码中更容易处理空指针问题。
1.Nil指针的表示:
C语言中,空指针用NULL表示,通常是一个宏定义为(void *)0。 Go语言中,空指针用nil表示,对于不同类型的指针,nil分别表示为(*int)(nil)、(*float64)(nil)等。Go语言中的nil是预定义的零值,不是一个宏或常量。
2.空指针的初始化:
在C语言中,指针的默认值是未定义的,除非显式地初始化为NULL或其他有效的地址值,否则它可能包含任意的垃圾值。 在Go语言中,指针的默认值是nil,表示指针未指向任何有效的内存地址。举个例子:
c
int main() {
int *ptr;
printf("Value at pointer: %d\n", *ptr);
return 0;
}
运行结果:
scss
Value at pointer: 4194304
上面这一段代码,C语言编译器会毫不犹豫的编译通过,并且在执行时并不会产生任何错误。但是对于程序员来说我们并不能知道这段代码最后打印出来的数是多少。这就是因为C语言编译器并不会把这样的野指针自动初始化成Null。
go
func main() {
var ptr *int // 声明一个空指针,值为nil
fmt.Println("%d", *ptr)
}
运行结果:
go
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x42deda]
goroutine 1 [running]:
main.main()
C:/Users/liuhz/Desktop/GO study/Arraytest.go:55 +0x3a
与之相对的这样一段GO语言代码,在运行时会报错并且明确告诉我们错误原因是在代码第55行发生了空指针解引用。可以看出在这里GO语言编译器帮我们把ptr自动初始化成了nil。
总结:
这篇文章中我们讲解了一些关于GO语言指针使用的基础知识。一句话总结一下文章中的主要内容:GO语言和C语言需要手动使用malloc等函数分配内存,而后需显式释放。空指针需手动赋值NULL。Go语言使用内置的make或new函数进行内存分配,具有自动垃圾回收,不需手动释放内存。空指针用nil表示,可直接判断,避免空指针解引用导致崩溃。