GO基础学习:数据类型

66 阅读24分钟

基本语法

变量以及初始化

变量声明,在Go语言中声明变量的时候,系统自动赋予它该类型的零值

var name type

与C语言不同的是,变量的类型一般是放在变量名称之后的。

Go语言基本类型

  • bool
  • string
  • int,int8,int16,int32,int64
  • uint, uint8,uint16,uint32,uint64,uintptr
  • byte //uint8的别名
  • rune //int32 的别名,代表一个Unicode码
  • float32,float64
  • complex64,complex128

当一个变量被声明之后,系统自动赋予它该类型的零值,int 为 0,float为0.0,bool为false,string为空字符串,指针为nil。

所有的内存在Go中都是经过初始化的。

  • 整型和浮点型变量的默认值为 0 和 0.0。
  • 字符串变量的默认值为空字符串。
  • 布尔型变量默认为false
  • 切片、函数、指针变量的默认为 nil。

引用类型

  • 函数
  • 接口
  • 指针
  • slice
  • map
  • chan

对于引用类型其零值均为nil,也就是其内部保存的引用地址是0x0这个nil。也就是其内部没有保存有初始化的内存空间

引用类型与基本类型的声明与初始化区别

引用类型

  • map
  • slice
  • chan

切片是引用类型

  • 基本类型不管是声明还是初始化,都会分配一个内存空间,如果是声明则默认初始化的值为该类型的0值
  • 引用类型只有初始化之后,这类型变量才会被分配一个内存空间,如果只是声明的话,不会分配内存空间。所以也就不能使用这个类型,如下 s2不能使用 s2[0] = 1 给切片的第一个元素赋值。而其零值是nil 。 但是当我们给引用类型变量初始化之后,它就有了实际存在的内存空间,就可以对该变量进行操作。
  • 引用类型声明之后,打印其保存的地址会得到一个0x0,表示还没有给其分配内存空间,也就是没有给它指定要保存的地址。其零值为其类型的零值,例如map为 map[],slice为[],chan通道为<nil>
  s1 := make([]int,3)
  var s2 []int
  fmt.Printf("s1 %p\n",s1)
  fmt.Println(s1)
  //s2未被分配初始内存地址,也是没有完成初始化
  fmt.Printf("s1 %p\n",s2)
  fmt.Println(s2)

  var a int = 3
  var b int

  fmt.Printf("s1 %p\n",&a)
  fmt.Println(a)
  //s2未被分配初始内存地址,也是没有完成初始化
  fmt.Printf("s1 %p\n",&b)
  fmt.Println(b)

nil

nil是Golang中预先声明的标识符(非关键保留字),其主要用来表示引用类型的零值,表示他们未初始化的值,

nil是Golang中唯一没有默认类型的非类型化的值,它不是一个未定义的状态,

a := nil

以上代码报错,因为编译器不知道它该给a分配什么类型的

nil ! = nil问题

  var p *int
  var i interface{}
  fmt.Println(p == i)

以上两个引用类型的变量i和p,因为没有初始化只是声明,所以其值是零值nil 。虽然他们两个都是nil值,但是并不相等,因为对应的两种不同类型的nil值

原因是,接口类型interface{}。一个接口类型interface{}要确定一个变量需要两个基础属性:Type和Value。

var p *int // {p类型 *int,值 nil} (T=*int,V=nil)
var i interface // {p类型 nil, 值 nil} (T=nil,V=nil)

fmt.Println(i == p) //{} (T=*int,V=nil) == (T=nil,V=nil) => false

从上诉代码可以看出,interface接口类型没有办法确定其类型,所以说与能够确定类型的*int就不能比较,虽然保存的值都是nil。

var p *int //{T = *int, V=nil}
var i interface //{T = nil, V = nil}

fmt.Println(p == nil) //true
fmt.Println(i == nil) //true

i = p 
fmt.Println(i == nil) // {T = nil, V = nil} == (T=nil, V=nil) -> false

因为这时候i虽然是*int类型,但是编译器在对他进行空值判断的时候,依旧会按照interface{}的nil(T=nil, V=nil)值来

请记住:如果接口中已存储任何具体值,那么接口将不会是 nil,详见反射定律。

零值

零值概念

  • 零值是变量没有做显示初始化时系统默认设置的值进场初始化。
  • 没有明确初始值的变量声明会被赋予它们的 零值
  • 当一个变量被声明之后,系统自动赋予它该类型的零值,int 为 0,float为0.0,bool为false,string为空字符串,指针为nil。
var m map[string]string
m1 := make(map[string]string)
m2 := map[string]string{}
fmt.Println(m,m1,m2)

以上的三种变量声明方式,都对map进行了初始化操作

  var m map[string]string   //使用了零值初始化
  m1 := make(map[string]string)  //使用make初始化
  m2 := map[string]string{}   //手动初始化

  if m == nil {
    fmt.Println("m is nil")
  }
  
  if m1 == nil {
    fmt.Println("m2 is nil")
  }

  if(m2 == nil) {
    fmt.Println("m2 is nil")
  }

各个类型的零值

基础类型零值
boolfalse
uint/uint8/uint16/uint32/uint640
int/int8/int16/int32/int640
float32/float640
complex64/complex1280
uintptr0
byte0
rune0
string""

由上表可以得出,基础类型中数据型零值都为0,而基础类型bool型值都为false

组合类型零值
interfacenil
slicenil
array定长的指定类型的零值的集合
mapnil
channil
struct内部属性所有零值集合
funcnil
所有类型的指针nil

标准格式

var 变量名 变量类型

批量格式

var(
   a int
   b string
   c []float32
   d func() bool
   e struct {
      x int
   }
)

可以批量的声明变量

简短格式

名字 := 表达式

简短模式的限制

  • 定义变量,同时显式初始化
  • 不能提供数据类型
  • 只能在函数内部使用
i,j := 0,1

如果使用全局变量或者方法接收器中的参数,那么就不能使用:= 来进行简短声明赋值

image_qt-h7Hhpjz.png

回顾C语言

在C语言中,变量在声明时,并不会对变量对应内存区域进行清理操作,此时,变量值可能是完全不可预期的结果,开发者需要习惯在使用C语言进行声明时要初始化操作,稍有不慎,就会产生不可预知的后果。

在网络上只有程序员才能看懂的“烫烫烫”和“屯屯屯”的梗,就来源于 C/C++ 中变量默认不初始化。

微软的 VC 编译器会将未初始化的栈空间以 16 进制的 0xCC 填充,而未初始化的堆空间使用 0xCD 填充,而 0xCCCC 和 0xCDCD 在中文的 GB2312 编码中刚好对应“烫”和“屯”字

编译器推导类型

var hp = 100

这种形式在标准格式的基础上,将int省略后,编译器会尝试根据等号右边的表达式推导hp的变量类型

等号右边的部分在编译原理里被称为右值。

下面是编译器根据右值推导变量类型完成初始化的例子

var attack = 40
var defence = 20
var damageRate float32 = 0.17
var demage = float32(attack-defence)*damageRate


其中

  • 第1行和2行,右值为整型,attack和defence 变量的类型为int
  • 第3行,表达式右值使用了0.17,由于Go语言和C语言一样,编译器会尽可能提供精确度,以避免计算中的精度损失,所以这里如果不指定damageRate 变量的类型,Go语言编译器会将 damageRate 类型推导为 float64,我们这里不需要 float64 的精度,所以需要强制指定类型为 float32。
  • 第 4 行,将 attack 和 defence 相减后的数值结果依然为整型,使用 float32() 将结果转换为 float32 类型,再与 float32 类型的 damageRate 相乘后,damage 类型也是 float32 类型

短变量声明并初始化

hp := 100

这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。

注意:由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

如以下代码

var hp int
hp := 10

在第一行中hp 已经被声明了。所以在使用推导声明写法的时候就会报错。

使用短变量声明接收返回值

conn,err := net.Dial("tcp","127.0.0.1:8000")

net.Dial提供按指定协议和地址发起网络连接,这个函数有两个返回值,一个是连接对象conn,一个是错误对象err, 如果是标准格式将会变成

var conn net.Conn
var err error
conn, err = net.Dial("tcp","127.0.0.1:8000")

这种表现形式就会显得十分繁琐

在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错,代码如下。

conn, err := net.Dial("tcp", "127.0.0.1:8080")
conn2, err := net.Dial("tcp", "127.0.0.1:8080")

如上图代码,编译器并不会报err重复定义。

多重赋值时,变量的左值和右值按从左到右的顺序赋值

短变量声明多次使用变量

如下代码所示,err在第一使用os.Create方法中,声明并创建了一个短变量。

在接下来的os.Stat 我们同样使用了这err变量来接收错误,因为这里fileinfo是没有声明过的必须使用:= 来声明并赋值,

f, err := os.Create("output.tar")
fileinfo, err := os.Stat("xms.enex")

但是如果只有err的话,就可以不使用:= 来声明并赋值,因为errr之前已经被声明过了,所以只需要使用=赋值即可

f, err := os.Create("output.tar")
_, err := os.Stat("xms.enex")

匿名变量

类似于Dart中的_变量,任何类型都可以赋值给它,但是赋值给它之后,这个值都会被抛弃

builder(context,value)
//使用匿名变量
builder(_,value){
    //此时context被赋值给匿名变量,所以被抛弃
}

在golang中也有匿名变量,其实现形式如下

package main

import (
  "fmt"
)
func main() {
  a,_ := GetData()
  fmt.Println(a)
}

func GetData()(int,int){
  return 100,200
}


GetData函数返回两个int类型数据,我们使用a来接收第一个返回值,而使用匿名变量来获取第二个返回值,所以第二个返回值直接被抛弃。

匿名变量不会暂用内存空间,不会分配内存。匿名变量与匿名变量之间不会因为多次声明而无法使用

变量作用域

一个变量在程序中都有一定的作用范围,称之为作用域

  • 定义在函数内部变量称为局部变量
  • 定义在函数外部的变量称为全局变量
  • 定义在函数上的变量称为形式参数

局部变量

在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量

局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁

全局变量

在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然其他源文件要使用这个全局变量需要在其源文件中使用import关键字引入全局变量所在的源文件之后才能使用这个全局变量

全局变量声明必须以var开头,如果想要在外部包中使用全局变量的,其首字母必须大写

在Go语言中,全局变量和局部变量的名称可以相同,但是在函数体内局部变量会被优先考虑(最近原则)

如下代码,rate在源文件中是一个float32全局变量,而在main函数体内,声明了一个int 类型的rate,而最终在main函数体中打印的rate函数值,是3,也就是局部变量的类型int

package main

import (
  "fmt"
)


var rate float32 = 0.3

func main() {
  //a,_ := GetData()

  rate := 3
  fmt.Println(rate)
}

形式参数

定义在函数名后面的括号中的变量叫做形式参数。只在函数调用的时候生效,在函数结束的时候销毁。在函数未被调用之前,形参不占用实际的存储单元,也没有实际值。

GO整数类型

rune类型 等价于 int32,表示一个unicode码点 ,比如0000-007F:C0控制符及基本拉丁文,一个unicode

byte类型等价于uint8,因为一个字节是八位,所以一个byte等于uint8

unitptr是一种无符号

哪些情况下使用int和uint

实际使用中,切片或 map 的元素数量等都可以用 int 来表示。

反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint。

Go语言浮点类型

  • 一个float32类型的浮点数可以提供大约6个十进制数的精度,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大
  • 一个float64类型可以提供约15个十进制数的精度

Go语言复数

在计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)

Go语言中复数的类型有两种,一种是complex128(64位实数和虚数)。一种是complex64(32位实数和虚数)

复数的值有三个部分组成RE+IMi,其中RE表示实数部分,IM是虚数部分,RE和IM均为float类型,i表示虚数单位

声明复数的语法格式

var name complex128 = complex(x,y)

其中name表示复数变量名,complex128为复数的类型,x,y分别表示复数的实部和虚部

Go语言bool类型

Go语言对于值之间的比较有非常严格的限制,只有两个相同类型的值才可以进行比较。

Go语言字符串

  • 一个字符串是一个不可改变的字节序列
  • 字符串是UTF-8字符的一个序列(当字符为ASCII上的字母是占用一个字节,其他字符根据需要占用2到4个字节)

UTF-8是一种被广泛使用的编码格式,是文本文件的标准编码,其中包括XML和JSON在内都是使用该编码。由于该编码对占用字符长度不定性,在GO语言字符串也可能根据需要占用1到4个字节,这与其他编程语言如c++,java或者python不同(java始终需要使用2个字节)。GO语言这样做不仅减少了内存和硬盘的空间占用,同时也不用像其他语言那样需要对使用UTF-8字符集进行编码和解码。

字符串是一种值类型,且值不可变。字符串是字节定长的数组

转义字符

  • \n: 换行符
  • \r:回车符
  • \t:tab键
  • \u 或者 \U:Unicode字符
  • \\ : 反斜杠自身

打印unicode字符串,其中4EBA这个unicode码(U+4EBA)所对应的符号是人,

func GoString(){
   emoji := "\u4EBA"
  fmt.Println(emoji)
}

字符串拼接

使用+号拼接字符串

str := "Beginning of the string " +
"second part of the string"

也可以使用+=

s := "hel" + "lo,"
s += "world!"
fmt.Println(s) //输出 “hello, world!”

Go数据类型转化

valueOfTypeB = typeB(valueOfTypeA)

类型B的值 = 类型B(类型A)

a := 5.0

b := int(a)

Go语言指针

系统多少位代表其CPU一次能处理的数据大小,在32位系统中一个指针的大小是4个字节,在64位系统中一个指针的大小是8个字节

  • 8位表示CPU一次能够处理一个字节的数据,或者是一个字节的指令
  • 32位表示CPU一次能处理4个字节的数据或者指令
  • 64位表示CPU一次能处理8个字节的数据或指令

指针类型

因为有指针的这样约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发送偏移,从而避免了非法修改关键数据的问题,同时垃圾回收也比较容易对不会发生偏移的指针进行检索和回收

  • 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无需进行数据拷贝,类型指针不能进偏移和运算
  • 切片,由指向起始元素的原始指针,元素数量和容量类型组成

指针操作

使用取地址符号 &, 获取变量的地址

var v T //声明一个T类型的v变量
ptr = &v

其中v代表被取地址的变量,变量v的地址使用变量prt进行接收,ptr类型为T ,称为T的指针类型。 代表指针

使用指针获取指针指向的值

可以使用* 操作符,也就是指针取值,获取指针指向的值

func prtvalue(){
  var tip string= "coderjun is boss"
  ptr := &tip

  fmt.Printf("ptr type: %T \n",ptr)
  fmt.Printf("address  %p\n",ptr)

  //对ptr进行取值操作
  fmt.Printf("ptr所对应的值 %s\n",*ptr)
  //取值之后的类型
  fmt.Printf("type %T\n",*ptr)
}

其中fmt使用Printf可以按照格式化输出占位变量值,比如 %T输出类型,%s输出字符串,%p输出值

使用指针修改值

func swap(a,b *int){
  //位运算交换变量
  //*a = *a ^ *b
  //*b = *a ^ *b
  //*a = *a ^ *b
  //交换指针指向的值,对外部有效
  //t := *a
  //*a = *b
  //*b = t
  //交换指针的值,外部依旧没有改变
  //t := a
  //a = b
  //b = t
  //fmt.Printf("a %d b %d",*a,*b)
}

使用指针变量获取命令行的输入信息

Go语言内置的flag包实现了对命令行参数的解析,flag包使得开发命令行工具更为简单

package main

// 导入系统包
import (
    "flag"
    "fmt"
)

// 定义命令行参数
var mode = flag.String("mode", "", "process mode")

func main() {

    // 解析命令行参数
    flag.Parse()

    // 输出命令行参数
    fmt.Println(*mode)
}

使用new()函数创建指针

new(类型)

str := new(string)
*str = "GO语言YYDS"
fmt.Println(*str)

第一行使用new方法创建了一个string类型的指针

第二行将"GO语言YYDS"字符串写入,str指针指向的内存空间

最后一行打印出str所指向的内存空间的值

使用new内置函数创建其他引用类型的指针

因为map是引用类型,创建了一个map类型的指针,但是没有分配内存空间,所以无法存储数据

mx := new(map[string]string)  //因为map是引用类型,创建了一个map类型的指针,但是没有分配内存空间,所以无法存储数据
  ms := *mx
  ms["a"] = "b"
  fmt.Println("用户类型 ",ms)

但是new出来的指针变量可以保存其他该变量类型的地址

  mx := new(map[string]string)  //因为map是引用类型,创建了一个map类型的指针,但是没有分配内存空间,所以无法存储数据
  mp := make(map[string]string)
  mx = &mp
  ma := *mx
  ma["a"] = "b"
  fmt.Println("mx值",ma)

Go语言常量

常量是指在编译时期就被创建的值,即使是定义在函数内部也是如此,并且只能是布尔值,数字型,字符串型。由于编译时的限制,定义常量必须为能够被编译器求值的常量表达式

常量的定义格式和变量声明语法类似。

const name type = value

const pi = 3.1415926

因为常量必须在编译时确定,可以在其表达式涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

//正确做法
const c1 = 2/3
//错误做法
const c2 = getNumber()

批量生产常量,与变量一样。常量也可以批量进行生成

const (
  e = 2.32323
  pi = 3.1415
)

//变量批量生成
var (
  a int = 3
  b := 4
  
)

常量批量声明,除了第一个常量必须赋值之外,剩余行的常量如果没有初始赋值,那么就是用最近的一行(向前)已赋的值

const(
  xc = 3
  yc
  xm = 3.2
  xn
)

如上代码,yc使用的它前一行xc的值,xn使用它最近前一行的xm的值

iota常量生成器

常量的声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,

iota使用规则是,在第一行声明的常量所在行,iota会被置为0,然后每有一个常量声明+1

type Weekday int

const (
  Sunday Weekday = iota
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
)

使用type定义一个类型定义,其类型是int

iota在第一行初始化0,然后在每一行遇到常量自增1。

无类型常量

常量有任意一个可以确定的类型,例如int 或float64。但是许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量提供币基础类型更高的精度算数运算,一共有6种未明确类型的常量类型。分别是无类型的布尔值,无类型的整数,无类型的字符,无类型的浮点数,无类型的复数,无类型的字符串

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

以上代码输出如下

x 3.1415927
y 3.141592653589793
z (3.141592653589793+0i)

由上可以看出math.Pi是无类型的浮点数常量。可以直接用于任意需要浮点数或复数的地方。

package math

// Mathematical constants.
const (
  E   = 2.71828182845904523536028747135266249775724709369995957496696763 // https://oeis.org/A001113
  Pi  = 3.14159265358979323846264338327950288419716939937510582097494459 // https://oeis.org/A000796
  Phi = 1.61803398874989484820458683436563811772030917980576286213544862 // https://oeis.org/A001622

  Sqrt2   = 1.41421356237309504880168872420969807856967187537694807317667974 // https://oeis.org/A002193
  SqrtE   = 1.64872127070012814684865078781416357165377610071014801157507931 // https://oeis.org/A019774
  SqrtPi  = 1.77245385090551602729816748334114518279754945612238712821380779 // https://oeis.org/A002161
  SqrtPhi = 1.27201964951406896425242246173749149171560804184009624861664038 // https://oeis.org/A139339

  Ln2    = 0.693147180559945309417232121458176568075500134360255254120680009 // https://oeis.org/A002162
  Log2E  = 1 / Ln2
  Ln10   = 2.30258509299404568401799145468436420760110148862877297603332790 // https://oeis.org/A002392
  Log10E = 1 / Ln10
)

math包的源代码如上,通过查看源码发现,Pi的值是一个无类型的常量。由编译器根据接收该值变量的类型进行转化。

Go语言type关键字(类型别名)

type TypeAlias = Type

TypeAlias相当于Type类型的别名,其类型是Type,相当于一个人有名字,或者小名。在上面一部分中使用type定义了一个Weekday其类型是int

类别名与类定义

//类别名
type IntAlias = int
//类定义
type NewInt int

func typeDefine(){
  var a NewInt = 10
  fmt.Printf("类定义a的类型是 %T\n",a)

  var x IntAlias = 5
  fmt.Printf("类别名的类型是 %T\n",x)

}
类定义a的类型是 main.NewInt
类别名的类型是 int


看到输出我们可以发现

类定义:是真正定义了一个新的类型,NewInt 本身依然具备 int 类型的特性。

类别名:是给int这个类型换了个名称,其本质还是int类型

非本地类型不能定义方法

不能在非本地的类型中添加新的方法

package main
import (
  "time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}
func main() {
  var my MyDuration = MyDuration(1)
  my.EasySet("cccc")
}

编译器报错,不能在一个非本地类型的time.Duration上定义新的方法,非本地类型是指time.Duration不是在main中定义的,而是在time包中定义的,与main包不在同一个包中,因此不能为不在一个包中的类型定义方法

解决方案

  • 第一种将类型别名改为类型定义
  • 第二种将MyDuration的别名放在time包中

第一种解决方案实现

package main
import (
  "fmt"
  "time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration  time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
  fmt.Println("value is ",m,"name is ",a)
}
func main() {
  var my MyDuration = MyDuration(10)
  my.EasySet("cccc")
}

可以看见我们定义了一种MyDuration新的类型,其具有time.Duration的特性。

然后我们给该MyDuration类型,拓展出一个EasySet的方法,然后再main中创建一个my 的实例,使用拓展方法。

结构体成员嵌入时使用别名

首先我们定义了一个叫做Brand的类型,其具有struect的特性

然后我们给Brand这个类型拓展了一个show方法

接着我们给这个Brand类型取了一个别名,其实还是Brand类型

接着我们创建一个叫做Vehicle的结构体,其内部包含了一个FakeBrand类型,一个Brand类型

最后我们在main函数中,使用反射来获取Vehicle这个struct的数据类型。

package main
import (
  "fmt"
  "reflect"
)

type Brand struct {

}

func (b Brand) show(){

}

type FakeBrand = Brand

type Vehicle struct {
  FakeBrand
  Brand
}

func main() {
  var a Vehicle
  a.FakeBrand.show()

  ta := reflect.TypeOf(a)

  for i:= 0; i < ta.NumField();i++{
    f := ta.Field(i)
    fmt.Printf("FieldName %v ,FieldType: %v \n",f.Name,f.Type)
  }
}

FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand

最后发现这个Vehicle中的两个类型其名称不一样,但是其类型是一样的都是Brand类型,因为FakeBrand只是Brand的一个类型别称,其类型还是Brand

watting fix 给类型拓展方法

func (t Type) methodName(参数) 返回值 {
}


///如果不需要该类型的值t,可以省略
func(Type) methodName(参数) 返回值 {
}

给Type这个类型拓展一个方法,其中(t Type)表示要拓展的类型,t表示这个类型的值,Type表示这个类型。其中类型的值可以省略。

命名规范

  • 为变量,函数,常量命名时采用小驼峰命名法,例如setName,getVal,一般来说小驼峰用于本包访问,而大驼峰SetName,GetVal用于提供给其他包访问
  • 一般给结构体命名都是使用大驼峰

结构体和类之间的区别

  • 结构体是很多数据的结构,其内部不能有对这些数据的操作,而class类是数据以及对这些数据的操作的封装,是面向对象的基础
  • class对成员变量有访问权限的控制,而struct没有,任何结构体外可以访问结构体内任何一个变量,而在类外,则不能访问类的私有成员变量
  • 结构体是值类型,而类是引用类型。大多数情况下该类型只是一些数据,使用结构体能比使用类获得更佳的性能
  • 使用结构体数组效率比类数组的效率要高(不需要装箱和拆箱)

值类型与引用类型

  • 值类型:byte,short,int, long,float,double,decimal,char,bool,struct

    值类型变量声明后,不管是否已经赋值,编译器为其分配内存。

    值类型的实例通常是在线程栈上分配(静态分配),但在某些情形下可以存储在堆中,

  • 引用类型:class

    当声明一个类时,只在栈中分配一小片内存用于容纳一个地址,而此时并没有为其分配堆上的空间,当使用new创建一个类的实例时,分配堆上的空间,并将堆上的空间的地址保存在栈上分配的小片空间中。

    引用类型的对象总是在进程堆中分配内存(动态分配)

#import "ViewController.h"
typedef  struct  {
    NSString* name;
}CatStruect;

@interface CatModel:NSObject
@property(strong,nonatomic) NSString* name;
@end

@implementation CatModel


@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
  
    ///引用类型,将model的类的值赋值给aModel,此时并没有产生新的CatModel对象,
    //因为class是引用类型,赋值的是在栈上面的指针,这个指针指向的是在堆中分配好的CatModel实例,所以aModel相当于是另一个指向堆中对象实例的指针
    CatModel* model = [[CatModel alloc] init];
    model.name = @"coderxx";
    CatModel* aModel = model;
    [self changeModelName:aModel];
    NSLog(@"name is %@",model.name);
    
    ///值引用,因为结构体是值类型,下面的catS是静态分配(也就是编译时分配的内存)。
    ///其对应的结构体保存在栈上面。当将catS的值赋值给modelS的时候,是直接在catS的值拷贝到modelS上,相当于在栈上对应了两个CatStruect。所以两个变量能够独立修改,互不影响
    CatStruect catS = {@"myname"};
    CatStruect modelS = catS;
    modelS.name = @"models paid";
    NSLog(@"catS s name is %@",catS.name);
    NSLog(@"modelS s name is %@",modelS.name);

    // Do any additional setup after loading the view.
}

- (void) changeModelName:(CatModel*) model {
    model.name = @"hello";
}