结构体声明和使用陷阱|学习笔记

简介: 快速学习结构体声明和使用陷阱

开发者学堂课程【Go 语言核心编程 - 面向对象、文件、单元测试、反射、TCP 编程:结构体声明和使用陷阱】学习笔记,与课程紧密联系,让用户快速学习知识。

课程地址:https://developer.aliyun.com/learning/course/626/detail/9669


结构体声明和使用陷阱

 

内容介绍:

一、结构体声明

二、结构体的字段/属性

 

一、结构体声明

这部分内容在之前的课程中已经进行过实际应用,如我们上节课解决养猫问题时的这段代码就属于声明结构体部分:

type Cat struct {

Name string

Age int

Color int

Hobby string

}

今天对结构体声明作系统的讲述。其基本语法如下:

1.基本语法

type 标识符/结构体名称 struct {

field1 type

field2 type

}

其中type与struct是关键字,在编程过程中不可修改,而结构体名称属于标识符,可根据编程需求自行选择,如Cat、Student等。下方会写入结构体的字段,字段数目依编程需求而定。写入字段时,第一部分是字段名称,如Name、Age等,第二部分是字段的类型,如int、string等。

2.案例

如以下代码为结构体声明部分:

type Student struct {

Name string

Age int

Score float32

}

其中,Student是结构体名称,其首字母大写,说明可以被其他的包

引用;Name、Age、Score是字段名,其首字母大写,说明其包含

的数据可以被其他包引用,若小写,则说明该数据是私有的,只能在

本包使用,这些规则与之前所学的变量的作用域要求一致。

 

二、结构体的字段/属性

1. 基本介绍

(1)从概念或名称上看:结构体字段=属性=field,由于教材翻译不一致,可能有这3种名称,但这三者在概念上一致(我们授课过程中统一称为字段)。

(2)字段是结构体的一个组成部分,一般是基本数据类型、数组,也可以是引用类型(map、指针或slice)。如解决养猫问题时Cat结构体的Name string就是基本数据类型,也可以是数组,如猫要进行测试,其包含3个项目的测试分数,故可以定义一个字段Score,它是数组类型,如下:

package main

import (

"fmt"

)

type Cat struct {

Name string

Age int

Color string

Hobby string

Scores [3]int

}

func main() {

var cat1 Cat

fmt.printf("cat1的地址=%p\n",&cat1)

cat1.Name = "小白"

cat1.Age = 3

cat1.Color = "白色"

cat1.Hobby = "吃<・)))><<"

fmt.Println("猫猫的信息如下:")

fmt.Println("name=",cat1.Name)

fmt.Println("age=",cat1.Age)

fmt.Println("color=",cat1.Color)

fmt.Println("hobby=",cat1.Hobby)

}

输出结果:

cat1的地址=0xc0420340c0

cat1=<小白 3 白色 吃<・)))><<  [0 0 0]>

猫猫的信息如下:

name= 小白

age= 3

color= 白色

hobby= 吃<・)))><<

结构体声明中该结构体Cat包含了数组的相关字段,而该数组是一个包含3个元素的一维数组,而由于未对数组类型的字段Scores赋值,故输出结果中Scores字段的输出结果中3项测试成绩均为默认值“0”;若此处的数组是二维数组,仍然可以运行并输出。此处不多作赘述。

2. 注意事项和细节说明

(1)字段声明的语法同变量声明的语法一致,声明方法:字段名 字段类型

(2)字段的类型可以为:基本类型、数组或引用类型

(3)在创建一个结构体变量后,如果没有给字段赋值,它都对应一个零值(默认值),其规则同前面所讲的相同:

①bool类型是false;数值(整型及浮点型)是0;字符串是""。

②数组类型的默认值与其元素类型相关,如Scores [3]int对应的是[0 0 0 ];当然,数组类型的默认值也取决于数组的类型,此处的数组类型为int,故默认值为[0 0 0] ,若此处的数组类型为string,则对应的默认值为["" "" ""], 即三个空字符串。

③若该字段的数据类型为指针、slice、map,则默认值为nil,即还没有分配空间。也就是说,若该字段的数据类型为指针、slice、map,要先进行make操作,才可以使用。

案例演示:

package main

import (

"fmt"

)

type Person struct {

Name string

Age int

Scores [5]float64  

ptr *int//指针,基本数据空间指针为new

slice [ ]int //切片

map1 map[string]string //map

}

func main() {

//定义一个结构体变量

var p1 Person

//结合上节课所学的结构体变量内存布局示意图可分析得到,前三个结构体变量Name、Age、Score已经可以输出默认值,而后面三个字段ptr、slice、map 均为引用类型,故它们还未指向地址空间,它们的默认值是nil,表示还未分配空间,还不能被使用。

fmt.Println(p1)

//输出结果为<  0 [0 0 0 0 0]  [ ] map[ ]>。Name是string类型,显示为空字符串;Age为int型,显示为默认值0;Scores是一维数组,其中包含5个int型的数据,皆显示默认值0;而指针部分显示为< nil>,切片显示为空,map也显示为空,换言之,这三个引用类型的变量虽然显示不同,但实际上均显示默认值nil,表示未分配空间。以下代码可作检验:

if p1.ptr == nil {

fmt.Println("ok1")

}

//输出结果为“ok1”,即结构体变量p1指向指针,由于未经过make,输出结果显示为nil;

if p1.slice == nil {

fmt.Println("ok2")

}

//输出结果为“ok2”,即结构体变量p1指向切片,由于未经过make,输出结果显示为nil;

if p1.map1 == nil {

fmt.Println("ok3")

}

//输出结果为“ok3”,即结构体变量p1指向map,由于未经过make,输出结果显示为nil;

//使用slice,一定要make。若直接输入p1.slice[0] =100

fmt.Println(p1),系统报错。

p1.slice = make([ ]int,10)

p1.slice[0] =100

fmt.Println(p1)

}

//输出结果为< 0 [0 0 0 0 0]  [100 0 0 0 0 0 0 0 0 0 ] map[ ]>。原因在于:与原先未对切片make空间时相比,为切片make了10个数据,且对第一个数据赋值为100。

//map与slice相同,也必须要先make,才能使用。若直接输入p1.map["key1"] = "tom~"  fmt.Println(p1),则由于未使用make,而直接创建字段,系统报错

p1.map1 = make(map[string ]string)

//此处的空间大小无须设定,只要分配有空间,会随着数据变化自行增长

p1.map["key1"] = "tom~"  

fmt.Println(p1)

}

//输出结果为< 0 [0 0 0 0 0]  [100 0 0 0 0 0 0 0 0 0 ] map[key1:tom~ ]>。

重点强调:使用slice与map时,一定要先make空间,才能正常使用。

未赋值及未make空间前(不含a步骤),结构体内存布局如下:

image.png经make(图中a步骤)之后,指针、切片和map都会指向一个引用空间,存放其地址。

(4)不同结构体的变量的字段都是独立的,互不影响,一个结构体字段的更改,不影响另外一个。结构体是值类型。

案例说明:

package main

import (

"fmt"

)

type Monster struct {

Name string

Age int

}

func main() {

var monster1 Monster

monster1.Name = "牛魔王"

monster1.Age = 500

//操作1

monster2 :=monster1  //将monster1赋值于monster2

fmt.Println("monster1=",monster1)

fmt.Println("monster2=",monster2)

//输出结果为:monster1=<牛魔王 500> monster2=<牛魔王 500>,两个输出内容相同

// 操作2

monster2 :=monster1  //结构体是值类型,默认为值拷贝

monster2.Name = "青牛精"

fmt.Println("monster1=",monster1)

fmt.Println("monster2=",monster2)

 //输出结果为:monster1=<牛魔王 500>  monster2=<青牛精

500>,两个monster输出内容不同,说明将monster2的名字

501>修改,monster1不会发生变化。因为结构体是值类型,将

monster1赋值给monster2时,进行的是值拷贝。若要通过改变monster2以改变monster1,则应进行地址的拷贝,即输入monster2 :=&monster1,但此操作会将monster2变为一个指针,运行时系统可能会报错,后面内容会作讲解。

}

以下为该段代码内存布局的变化的示意图:

image.png

说明:

①var monster1 Monster

monster1.Name = "牛魔王"

monster1.Age = 500

定义结构体变量monster1,并为字段赋值,其指向一个结构体空间a。

②monster2 :=monster1

创建一个结构体变量monster2,并指向monster1指向的结构体空间a所拷贝的结构体b。此时monster1与monster2是两个完全独立的结构体。

③monster2.Name = "青牛精"

将monster2的名字改为“青牛精”,并不影响monster1的值。

若要改变monster2的值以改变monster1的值,则要将monster1的地址赋给monster2,让monster2也指向monster1指向的结构体空间a,这一操作会在后面讲述。

本节课要重点掌握:

1)引用类型作为结构体的字段时,一定要make;

2)结构体是值类型,一个变量对一个变量赋值时默认为值拷贝。要通过改变一个变量进而改变另一个变量,那么要传递的应是地址。

相关文章
|
6月前
|
编译器 Shell Linux
C语言的本质(六):链接详解-定义和声明
C语言的本质(六):链接详解-定义和声明
134 0
|
1月前
|
编译器 C语言
2.4 声明变量的4个理由
将所有变量集中声明,便于读者理解和查找,尤其当变量名具有描述性时效果更佳。若变量名不够清晰,应在注释中解释其含义,以提高代码可读性。声明变量有助于编程前规划,明确所需输入、期望输出及最佳数据表示方式,同时有助于发现潜在错误,如变量名拼写错误导致的问题。根据C99之前的规范,变量声明应置于块顶部,但C99允许按需声明,有助于避免遗漏变量赋值的情况。然而,许多编译器尚未完全支持C99标准。
39 5
|
5月前
|
C语言
C语言学习记录——枚举(定义、与结构体的区别、优点)
C语言学习记录——枚举(定义、与结构体的区别、优点)
55 3
|
5月前
|
存储 编译器 C语言
C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)一
C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)一
56 2
|
5月前
|
编译器 Linux C语言
C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)二
C语言学习记录——结构体(声明、初始化、自引用、内存对齐、结构体设计、修改默认对齐数、结构体传参)二
50 1
|
6月前
|
存储 C语言
C语言中的结构体与函数传递技术详解
C语言中的结构体与函数传递技术详解
193 1
|
6月前
|
算法 C语言
C语言函数参数的声明及调用
C语言函数参数的声明及调用
44 1
|
C语言
7.10 【C语言】关于变量的声明和定义
7.10 【C语言】关于变量的声明和定义
55 0
|
C语言
C语言定义结构体的几种方法
C语言定义结构体的几种方法
174 0
|
编译器 C语言
关于c语言结构体成员变量访问方式的一点思考
关于c语言结构体成员变量访问方式的一点思考
117 0
下一篇
无影云桌面