c语言函数调用规则

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c语言函数调用规则相关的知识,希望对你有一定的参考价值。

参考技术A _stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

_cdecl 按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)是C和C++程序的默认调用约定。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。

_fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,格式为@functionname@number。
参考技术B c语言函数有返回和无返回两种形式 参考技术C C语言调用函数就是先定义并声明函数,之后再根据定义函数的格式调用。
下面举例来说明函数调用方法:
#include<stdio.h>
int
fun(int
x,
int
y);
//
函数声明,如果函数写在被调用处之前,可以不用声明
void
main()

int
a=1,
b=2,
c;
c
=
fun(a,
b);
//
函数的调用,调用自定义函数fun,其中a,b为实际参数,传递给被调用函数的输入值

//
自定义函数fun
int
fun(int
x,
int
y)
//
函数首部

//
中的语言为函数体
return
x>y
?
x
:
y;
//
返回x和y中较大的一个数
参考技术D 语言的作用域规则”是一组确定一部分代码是否“可见”或可访问另一部分代码和数据的规则。
C语言中的每一个函数都是一个独立的代码块。一个函数的代码块是隐藏于函数内部的,不能被任何其它函数中的任何语句(除调用它的语句之外)所访问(例如,用g o t o语句跳转到另一个函数内部是不可能的)。构成一个函数体的代码对程序的其它部分来说是隐蔽的,它既不能影响程序其它部分,也不受其它部分的影响。换言之,由于两个函数有不同的作用域,定义在一个函数内部的代码数据无法与定义在另一个函数内部的代码和数据相互作用。
C语言中所有的函数都处于同一作用域级别上。这就是说,把一个函数定义于另一个函数内部是不可能的。
4.2.1 局部变量
在函数内部定义的变量成为局部变量。在某些C语言教材中,局部变量称为自动变量,这就与使用可选关键字a u t o定义局部变量这一作法保持一致。局部变量仅由其被定义的模块内部的语句所访问。换言之,局部变量在自己的代码模块之外是不可知的。切记:模块以左花
括号开始,以右花括号结束。
对于局部变量,要了解的最重要的东西是:它们仅存在于被定义的当前执行代码块中,即局部变量在进入模块时生成,在退出模块时消亡。
定义局部变量的最常见的代码块是函数。例如,考虑下面两个函数。

整数变量x被定义了两次,一次在func1()中,一次在func2()中。func1()和func2()中的x互不相关。其原因是每个x作为局部变量仅在被定义的块内可知。
语言中包括了关键字auto,它可用于定义局部变量。但自从所有的非全局变量的缺省值假定为auto以来,auto就几乎很少使用了,因此在本书所有的例子中,均见不到这一关键字。
在每一函数模块内的开始处定义所有需要的变量,是最常见的作法。这样做使得任何人读此函数时都很容易,了解用到的变量。但并非必须这样做不可,因为局部变量可以在任何模块中定义。为了解其工作原理,请看下面函数。

这里的局部变量s就是在if块入口处建立,并在其出口处消亡的。因此s仅在if块中可知,而在其它地方均不可访问,甚至在包含它的函数内部的其它部分也不行。
在一个条件块内定义局部变量的主要优点是仅在需要时才为之分配内存。这是因为局部变量仅在控制转到它们被定义的块内时才进入生存期。虽然大多数情况下这并不十分重要,但当代码用于专用控制器(如识别数字安全码的车库门控制器)时,这就变得十分重要了,因为这时随机存储器(RAM)极其短缺。
由于局部变量随着它们被定义的模块的进出口而建立或释放,它们存储的信息在块工作结束后也就丢失了。切记,这点对有关函数的访问特别重要。当访问一函数时,它的局部变量被建立,当函数返回时,局部变量被销毁。这就是说,局部变量的值不能在两次调用之间保持。
4.2.2全局变量
与局部变量不同,全局变量贯穿整个程序,并且可被任何一个模块使用。它们在整个程序执行期间保持有效。全局变量定义在所有函数之外,可由函数内的任何表达式访问。在下面的程序中可以看到,变量count定义在所有函数之外,函数main()之前。但其实它可以放置在任何第一次被使用之前的地方,只要不在函数内就可以。实践表明,定义全局变量的最佳位置是在程序的顶部。

仔细研究此程序后,可见变量count既不是main()也不是func1()定义的,但两者都可以使用它。函数func2()也定义了一个局部变量count。当func2访问count时,它仅访问自己定义的局部变量count,而不是那个全局变量count。切记,全局变量和某一函数的局部变量同名时,该函数对该名的所有访问仅针对局部变量,对全局变量无影响,这是很方便的。然而,如果忘记了这点,即使程序看起来是正确的,也可能导致运行时的奇异行为。
全局变量由C编译程序在动态区之外的固定存储区域中存储。当程序中多个函数都使用同一数据时,全局变量将是很有效的。然而,由于三种原因,应避免使用不必要的全局变量:
①不论是否需要,它们在整个程序执行期间均占有存储空间。②由于全局变量必须依靠外部定义,所以在使用局部变量就可以达到其功能时使用了全局变量,将降低函数的通用性,这是因为它要依赖其本身之外的东西。③大量使用全局变量时,不可知的和不需要的副作用将
可能导致程序错误。如在编制大型程序时有一个重要的问题:变量值都有可能在程序其它地点偶然改变。
结构化语言的原则之一是代码和数据的分离。C语言是通过局部变量和函数的使用来实现这一分离的。下面用两种方法编制计算两个整数乘积的简单函数mul()。
通用的专用的
mul(x,y) intx,y;
intx,y; mul()

return(x*y);return(x*y);

两个函数都是返回变量x和y的积,可通用的或称为参数化版本可用于任意两整数之积,而专用的版本仅能计算全局变量x和y的乘积。
4.2.3动态存储变量
从变量的作用域原则出发,我们可以将变量分为全局变量和局部变量;换一个方式,从变量的生存期来分,可将变量分为动态存储变量及静态存储变量。
动态存储变量可以是函数的形式参数、局部变量、函数调用时的现场保护和返回地址。
这些动态存储变量在函数调用时分配存储空间,函数结束时释放存储空间。动态存储变量的定义形式为在变量定义的前面加上关键字“auto”,例如:
auto int a,b,c;
“auto”也可以省略不写。事实上,我们已经使用的变量均为省略了关键字“auto”的动态存储变量。有时我们甚至为了提高速度,将局部的动态存储变量定义为寄存器型的变量,定义的形式为在变量的前面加关键字“register”,例如:
register int x,y,z;
这样一来的好处是:将变量的值无需存入内存,而只需保存在CPU内的寄存器中,以使速度大大提高。由于CPU内的寄存器数量是有限的,不可能为某个变量长期占用。因此,一些操作系统对寄存器的使用做了数量的限制。或多或少,或根本不提供,用自动变量来替代。
4.2.4静态存储变量
在编译时分配存储空间的变量称为静态存储变量,其定义形式为在变量定义的前面加上关键字“static”,例如:
static int a=8;
定义的静态存储变量无论是做全程量或是局部变量,其定义和初始化在程序编译时进行。
作为局部变量,调用函数结束时,静态存储变量不消失并且保留原值。

从上述程序看,函数f()被三次调用,由于局部变量x是静态存储变量,它是在编译时分配存储空间,故每次调用函数f()时,变量x不再重新初始化,保留加1后的值,得到上面的输出。

CGO类型转换

类型转换

最初CGO是为了达到方便从Go语言函数调用C语言函数(用C语言实现Go语言声明的函数)以复用C语言资源这一目的而出现的(因为C语言还会涉及回调函数,自然也会涉及到从C语言函数调用Go语言函数(用Go语言实现C语言声明的函数))。现在,它已经演变为C语言和Go语言双向通讯的桥梁。要想利用好CGO特性,自然需要了解此二语言类型之间的转换规则,这是本节要讨论的问题。

数值类型

在Go语言中访问C语言的符号时,一般是通过虚拟的“C”包访问,比如C.int对应C语言的int类型。有些C语言的类型是由多个关键字组成,但通过虚拟的“C”包访问C语言类型时名称部分不能有空格字符,比如unsigned int不能直接通过C.unsigned int访问。因此CGO为C语言的基础数值类型都提供了相应转换规则,比如C.uint对应C语言的unsigned int

技术图片

需要注意的是,虽然在C语言中int、short等类型没有明确定义内存大小,但是在CGO中它们的内存大小是确定的。在CGO中,C语言的int和long类型都是对应4个字节的内存大小size_t类型可以当作Go语言uint无符号整数类型对待。

CGO中,虽然C语言的int固定为4字节的大小,但是Go语言自己的int和uint却在32位和64位系统下分别对应4个字节和8个字节大小。如果需要在C语言中访问Go语言的int类型,可以通过GoInt类型访问,GoInt类型在CGO工具生成的_cgo_export.h头文件中定义。其实在_cgo_export.h头文件中,每个基本的Go数值类型都定义了对应的C语言类型,它们一般都是以单词Go为前缀。下面是64位环境下,_cgo_export.h头文件生成的Go数值类型的定义,其中GoInt和GoUint类型分别对应GoInt64和GoUint64:

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef float GoFloat32;
typedef double GoFloat64;

除了GoIntGoUint之外,我们并不推荐直接访问GoInt32、GoInt64等类型。更好的做法是通过C语言的C99标准引入的<stdint.h>头文件。为了提高C语言的可移植性,在<stdint.h>文件中,不但每个数值类型都提供了明确内存大小,而且和Go语言的类型命名更加一致。

Go语言类型<stdint.h>头文件类型对比如表

技术图片

前文说过,如果C语言的类型是由多个关键字组成,则无法通过虚拟的“C”包直接访问(比如C语言的unsigned short不能直接通过C.unsigned short访问)。但是,在<stdint.h>中通过使用C语言的typedef关键字将unsigned short重新定义为uint16_t这样一个单词的类型后,我们就可以通过C.uint16_t访问原来的unsigned short类型了。对于比较复杂的C语言类型,推荐使用typedef关键字提供一个规则的类型命名,这样更利于在CGO中访问。

Go 字符串和切片

在CGO生成的_cgo_export.h头文件中还会为Go语言的字符串、切片、字典、接口和管道等特有的数据类型生成对应的C语言类型:

typedef struct { const char *p; GoInt n; } GoString;
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

不过需要注意的是,其中只有字符串和切片在CGO中有一定的使用价值,因为CGO为他们的某些GO语言版本的操作函数生成了C语言版本,因此二者可以在Go调用C语言函数时马上使用;而CGO并未针对其他的类型提供相关的辅助函数,且Go语言特有的内存模型导致我们无法保持这些由Go语言管理的内存指针,所以它们C语言环境并无使用的价值。

在导出的C语言函数中我们可以直接使用Go字符串和切片。假设有以下两个导出函数:

//export helloString
func helloString(s string) {}

//export helloSlice
func helloSlice(s []byte) {}

CGO生成的_cgo_export.h头文件会包含以下的函数声明:

extern void helloString(GoString p0);
extern void helloSlice(GoSlice p0);

不过需要注意的是,如果使用了GoString类型则会对_cgo_export.h头文件产生依赖,而这个头文件是动态输出的。

Go1.10针对Go字符串增加了一个_GoString_预定义类型,可以降低在cgo代码中可能对_cgo_export.h头文件产生的循环依赖的风险。我们可以调整helloString函数的C语言声明为:

extern void helloString(_GoString_ p0);

因为_GoString_是预定义类型,我们无法通过此类型直接访问字符串的长度和指针等信息。Go1.10同时也增加了以下两个函数用于获取字符串结构中的长度和指针信息:

size_t _GoStringLen(_GoString_ s);
const char *_GoStringPtr(_GoString_ s);

更严谨的做法是为C语言函数接口定义严格的头文件,然后基于稳定的头文件实现代码。

结构体、联合、枚举类型

C语言的结构体、联合、枚举类型不能作为匿名成员被嵌入到Go语言的结构体中。在Go语言中,我们可以通过C.struct_xxx来访问C语言中定义的struct xxx结构体类型。结构体的内存布局按照C语言的通用对齐规则,在32位Go语言环境C语言结构体也按照32位对齐规则,在64位Go语言环境按照64位的对齐规则。对于指定了特殊对齐规则的结构体,无法在CGO中访问。

结构体的简单用法如下:

/*
struct A {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.i)
    fmt.Println(a.f)
}

如果结构体的成员名字中碰巧是Go语言的关键字,可以通过在成员名开头添加下划线来访问:

/*
struct A {
    int type; // type 是 Go 语言的关键字
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 对应 type
}

但是如果有2个成员:一个是以Go语言关键字命名,另一个刚好是以下划线和Go语言关键字命名,那么以Go语言关键字命名的成员将无法访问(被屏蔽):

/*
struct A {
    int   type;  // type 是 Go 语言的关键字
    float _type; // 将屏蔽CGO对 type 成员的访问
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a._type) // _type 对应 _type
}

C语言结构体中位字段对应的成员无法在Go语言中访问,如果需要操作位字段成员,需要通过在C语言中定义辅助函数来完成。对应零长数组的成员,无法在Go语言中直接访问数组的元素,但其中零长的数组成员所在位置的偏移量依然可以通过unsafe.Offsetof(a.arr)来访问。

/*
struct A {
    int   size: 10; // 位字段无法访问
    float arr[];    // 零长的数组也无法访问
};
*/
import "C"
import "fmt"

func main() {
    var a C.struct_A
    fmt.Println(a.size) // 错误: 位字段无法访问
    fmt.Println(a.arr)  // 错误: 零长的数组也无法访问
}

在C语言中,我们无法直接访问Go语言定义的结构体类型。

对于联合类型,我们可以通过C.union_xxx来访问C语言中定义的union xxx类型。但是Go语言中并不支持C语言联合类型,它们会被转为对应大小的字节数组

/*
#include <stdint.h>

union B1 {
    int i;
    float f;
};

union B2 {
    int8_t i8;
    int64_t i64;
};
*/
import "C"
import "fmt"

func main() {
    var b1 C.union_B1;
    fmt.Printf("%T
", b1) // [4]uint8

    var b2 C.union_B2;
    fmt.Printf("%T
", b2) // [8]uint8
}

如果需要操作C语言的联合类型变量,一般有三种方法:第一种是在C语言中定义辅助函数;第二种是通过Go语言的"encoding/binary"手工解码成员(需要注意大端小端问题);第三种是使用unsafe包强制转型为对应类型(这是性能最好的方式)。下面展示通过unsafe包访问联合类型成员的方式:

/*
#include <stdint.h>

union B {
    int i;
    float f;
};
*/
import "C"
import "fmt"

func main() {
    var b C.union_B;
    fmt.Println("b.i:", *(*C.int)(unsafe.Pointer(&b)))
    fmt.Println("b.f:", *(*C.float)(unsafe.Pointer(&b)))
}

虽然unsafe包访问最简单、性能也最好,但是对于有嵌套联合类型的情况处理会导致问题复杂化。对于复杂的联合类型,推荐通过在C语言中定义辅助函数的方式处理。

对于枚举类型,我们可以通过C.enum_xxx来访问C语言中定义的enum xxx结构体类型。

/*
enum C {
    ONE,
    TWO,
};
*/
import "C"
import "fmt"

func main() {
    var c C.enum_C = C.TWO
    fmt.Println(c)
    fmt.Println(C.ONE)
    fmt.Println(C.TWO)
}

在C语言中,枚举类型底层对应int类型,支持负数类型的值。我们可以通过C.ONE、C.TWO等直接访问定义的枚举值。

数组、字符串和切片

在C语言中,数组名其实对应于一个指针,指向特定类型特定长度的一段内存,但是这个指针不能被修改;当把数组名传递给一个函数时,实际上传递的是数组第一个元素的地址。为了讨论方便,我们将一段特定长度的内存统称为数组。C语言的字符串是一个char类型的数组,字符串的长度需要根据表示结尾的NULL字符的位置确定。C语言中没有切片类型。

在Go语言中,数组是一种值类型,而且数组的长度是数组类型的一个部分。Go语言字符串对应一段长度确定的只读byte类型的内存。Go语言的切片则是一个简化版的动态数组。

Go语言和C语言的数组、字符串和切片之间的相互转换可以简化为Go语言的切片和C语言中指向一定长度内存的指针之间的转换。

CGO的C虚拟包提供了以下一组函数,用于Go语言和C语言之间数组和字符串的双向转换:

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller‘s responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller‘s responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

其中C.CString针对输入的Go字符串,克隆一个C语言格式的字符串;返回的字符串由C语言的malloc函数分配,不使用时需要通过C语言的free函数释放。C.CBytes函数的功能和C.CString类似,用于从输入的Go语言字节切片克隆一个C语言版本的字节数组,同样返回的数组需要在合适的时候释放。C.GoString用于将从NULL结尾的C语言字符串克隆一个Go语言字符串。C.GoStringN是另一个字符数组克隆函数。C.GoBytes用于从C语言数组,克隆一个Go语言字节切片。

该组辅助函数都是以克隆的方式运行。当Go语言字符串和切片向C语言转换时,克隆的内存由C语言的malloc函数分配,最终可以通过free函数释放当C语言字符串或数组向Go语言转换时,克隆的内存由Go语言分配管理。通过该组转换函数,转换前和转换后的内存依然在各自的语言环境中,它们并没有跨越Go语言和C语言。克隆方式实现转换的优点是接口和内存管理都很简单,缺点是克隆需要分配新的内存和复制操作都会导致额外的开销

在reflect包中有字符串和切片的定义:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

如果不希望单独分配内存,可以在Go语言中直接访问C语言的内存空间:

package main

/*
#include <string.h>
char arr[10];
char *s = "Hello";
*/
import "C"
import (
	"fmt"
	"reflect"
	"unsafe"
)

func main() {
	// 通过 reflect.SliceHeader 转换
	var arr0 []byte
	var arr0Hdr = (*reflect.SliceHeader)(unsafe.Pointer(&arr0))
	arr0Hdr.Data = uintptr(unsafe.Pointer(&C.arr[0]))
	arr0Hdr.Len = 10
	arr0Hdr.Cap = 10
	fmt.Println(arr0)

	var s0 string
	var s0Hdr = (*reflect.StringHeader)(unsafe.Pointer(&s0))
	s0Hdr.Data = uintptr(unsafe.Pointer(C.s))
	s0Hdr.Len = int(C.strlen(C.s))
	fmt.Println(s0)
	// 通过切片语法转换
	arr1 := (*[31]byte)(unsafe.Pointer(&C.arr[0]))[:10:10]
	fmt.Println(arr1)
	
	sLen := int(C.strlen(C.s))
	s1 := string((*[31]byte)(unsafe.Pointer(C.s))[:sLen:sLen])
	fmt.Println(s1)
}

因为Go语言的字符串是只读的,用户需要自己保证Go字符串在使用期间,底层对应的C字符串内容不会发生变化、内存不会被提前释放掉。

在CGO中,会为字符串和切片生成和上面结构对应的C语言版本的结构体:

typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

在C语言中可以通过GoStringGoSlice来访问Go语言的字符串和切片。如果是Go语言中数组类型,可以将数组转为切片后再行转换。如果字符串或切片对应的底层内存空间由Go语言的运行时管理,那么在C语言中不能长时间保存Go内存对象

指针间的转换

在C语言中,不同类型的指针是可以显式或隐式转换的,如果是隐式只是会在编译时给出一些警告信息。但是Go语言对于不同类型的转换非常严格,任何C语言中可能出现的警告信息在Go语言中都可能是错误!指针是C语言的灵魂,指针间的自由转换也是cgo代码中经常要解决的第一个重要的问题。

在Go语言中两个指针的类型完全一致则不需要转换可以直接通用。如果一个指针类型是用type命令在另一个指针类型基础之上构建的,换言之两个指针底层是相同完全结构的指针,那么我我们可以通过直接强制转换语法进行指针间的转换。但是cgo经常要面对的是2个完全不同类型的指针间的转换,原则上这种操作在纯Go语言代码是严格禁止的。

cgo存在的一个目的就是打破Go语言的禁止,恢复C语言应有的指针的自由转换和指针运算。以下代码演示了如何将X类型的指针转化为Y类型的指针:

var p *X
var q *Y

q = (*Y)(unsafe.Pointer(p)) // *X => *Y
p = (*X)(unsafe.Pointer(q)) // *Y => *X

为了实现X类型指针到Y类型指针的转换,我们需要借助unsafe.Pointer作为中间桥接类型实现不同类型指针之间的转换。unsafe.Pointer指针类型类似C语言中的void*类型的指针。

技术图片

任何类型的指针都可以通过强制转换为unsafe.Pointer指针类型去掉原有的类型信息,然后再重新赋予新的指针类型而达到指针间的转换的目的。

数值和指针的转换

不同类型指针间的转换看似复杂,但是在cgo中已经算是比较简单的了。在C语言中经常遇到用普通数值表示指针的场景,也就是说如何实现数值和指针的转换也是cgo需要面对的一个问题。

为了严格控制指针的使用,Go语言禁止将数值类型直接转为指针类型!不过,Go语言针对unsafe.Pointr指针类型特别定义了一个uintptr类型。我们可以uintptr为中介,实现数值类型到unsafe.Pointr指针类型到转换。再结合前面提到的方法,就可以实现数值和指针的转换了。

下面流程图演示了如何实现int32类型到C语言的char*字符串指针类型的相互转换:

技术图片

package main

// char * Cc;
import "C"
import "unsafe"

var goi int32

func main() {
	goi = int32(uintptr(unsafe.Pointer(&C.Cc)))
	C.Cc = (* C.char)(unsafe.Pointer(uintptr(goi)))
}

转换分为几个阶段,在每个阶段实现一个小目标:首先是int32到uintptr类型,然后是uintptr到unsafe.Pointr指针类型,最后是unsafe.Pointr指针类型到*C.char类型。

切片间的转换

在C语言中数组也一种指针,因此两个不同类型数组之间的转换和指针间转换基本类似。但是在Go语言中,数组或数组对应的切片都不再是指针类型,因此我们也就无法直接实现不同类型的切片之间的转换。

不过Go语言的reflect包提供了切片类型的底层结构,再结合前面讨论到不同类型之间的指针转换技术就可以实现[]X和[]Y类型的切片转换:

var p []X
var q []Y

pHdr := (*reflect.SliceHeader)(unsafe.Pointer(&p))
qHdr := (*reflect.SliceHeader)(unsafe.Pointer(&q))

pHdr.Data = qHdr.Data
pHdr.Len = qHdr.Len * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])
pHdr.Cap = qHdr.Cap * unsafe.Sizeof(q[0]) / unsafe.Sizeof(p[0])

不同切片类型之间转换的思路是先构造一个空的目标切片,然后用原有的切片底层数据填充目标切片。如果X和Y类型的大小不同,需要重新设置Len和Cap属性。需要注意的是,如果X或Y是空类型,上述代码中可能导致除0错误,实际代码需要根据情况酌情处理。

下面演示了切片间的转换的具体流程:

技术图片

以上是关于c语言函数调用规则的主要内容,如果未能解决你的问题,请参考以下文章

C语言中取整的规则是啥?

CGO类型转换

CGO类型转换

c语言基本语法

C语言的语法规则是啥?

什么是C语言中的隐式函数声明?