正文
UE4 C++ 与标准 C++ 在某些方面有些区别。本文针对函数参数这块,做一个简要的分析,如文章有所疏漏,可以在下方留言提醒,不吝赐教。
在看此文之前,假设你已经对 UE4 C++有了基本的了解,以及对 C++ 有一定的编程经验。
如果你想直接看文章的结构及总结,可以直接翻到文章末尾的总结。
为方便起见,首先在 C++ 中创建一个 Actor
类。
并建立一个 public
变量 MyNumber
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int32 MyNumber = 0;
并在这个 Actor
类的上方,创建一个结构体,用于测试传递对象作为参数时的情况
USTRUCT(BlueprintType)
struct FMyTest
{
GENERATED_BODY()
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int32 X;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int32 Y;
FMyTest() :X(100), Y(200) {}
FMyTest(const int32& x, const int32& y) :X(x), Y(y) {}
FMyTest(const FMyTest& MyTest) :X(MyTest.X), Y(MyTest.Y) {}
};
然后在蓝图中创建一个 Actor
类,并继承这个 Actor
类,这样就可以开始我们的测试了。
一、值传递函数参数在蓝图中的表现形式
(1)int32
在 C++ 中,添加这个函数。
UFUNCTION(BlueprintCallable)
void test1_1(int32 number)
{
number++;
}
在蓝图中,可以看到为这样的形式,也就是正常的值传递参数
简单测试一下
由于 MyNumber
默认值为 0,传入 Test1_1
后为值传递,因此 MyNumber
的值不会由于传入了这个函数而发生变化,所以显然 print
的结果还是 0 .
(2)const int32
在 C++ 中,添加这个函数。
UFUNCTION(BlueprintCallable)
void test1_2(const int32 number)
{
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
在蓝图中,可以看到为这样的形式,也就是正常的值传递参数
由于 const
,那么 MyNumber
的值肯定不会发生变化了,并且在函数中也无法修改其值.
Log 信息为
(3)关于默认参数
无论是 int32
还是 const int32
,都可以在 C++ 中设定默认参数,并在蓝图中可以手动填充实参。
UFUNCTION(BlueprintCallable)
void test1_1(int32 number=100)
{
number++;
}
UFUNCTION(BlueprintCallable)
void test1_2(const int32 number=100)
{
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
在蓝图中打开,默认值就发生了变化
如果不在 C++ 中设定默认参数,则其默认值均为 0。
(4)FMyTest 和 const FMyTest
若为参数并非基本类型,而是其他类型,如一个类,如上方定义的FMyTest
UFUNCTION(BlueprintCallable)
void test1_3(FMyTest para1, const FMyTest para2)
{
UE_LOG(LogTemp, Warning, TEXT("Date1 : X = %d , Y = %d "), para1.X, para1.Y);
UE_LOG(LogTemp, Warning, TEXT("Date2 : X = %d , Y = %d "), para2.X, para2.Y);
}
在蓝图中显示为
由于蓝图无法给这个自定义类型自动生成默认值,所以在蓝图中不会像 int32
一样能直接填写默认值
测试一下
可以看到,调用了 FMyTest
这个结构体的默认构造函数。
FMyTest() :X(100), Y(200) {}
(5)FMyTest 的函数默认参数
如果我们想像 int32=100
这样写一个函数默认参数呢?自然我们就想这么去写下。
UFUNCTION(BlueprintCallable)
void test1_3(FMyTest para1, const FMyTest para2=FMyTest(200,400))
{
UE_LOG(LogTemp, Warning, TEXT("Date1 : X = %d , Y = %d "), para1.X, para1.Y);
UE_LOG(LogTemp, Warning, TEXT("Date2 : X = %d , Y = %d "), para2.X, para2.Y);
}
然后发现
也就是说,我们无法对自定义类用这种默认函数参数的形式。只有去掉了 UFUNCTION
之后才可以对自定义类用函数默认参数。
void test1_3(FMyTest para1,const FMyTest& para2= FMyTest(200, 400))
{
UE_LOG(LogTemp, Warning, TEXT("Date1 : X = %d , Y = %d "), para1.X, para1.Y);
UE_LOG(LogTemp, Warning, TEXT("Date2 : X = %d , Y = %d "), para2.X, para2.Y);
}
如果能在 C++ 中写自定义类型的函数默认参数,并用于蓝图,麻烦告知我一声,我谷歌了一圈没搜到。
二、引用传递函数参数在蓝图中的表现形式
(1)const int32&
其为 const
引用传递,即形参作为实参的别名传入函数中,相当于实参传入函数中,但不可更改实参的值。(虽然是 C++ 基本常识,在这还是提一下)。无论在蓝图还是 C++ 中,都符合这个规则。
a) 不带有默认参数
在 C++ 中,添加这个函数。
UFUNCTION(BlueprintCallable)
void test1_4(const int32& number)
{
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
在蓝图中,可以看到为这样的形式
测试一下,不填充这个参数的值,就会发生错误警告
传入一个参数后
b) 带有默认参数
UFUNCTION(BlueprintCallable)
void test1_4(const int32& number=100)
{
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
若我们写成这种方式,在 C++ 中就可以不传递函数参数,直接通过 test1_4()
的方式就能调用这个函数。但在蓝图中,当我们不连接任何参数时,一样会报错。
也就是说,无论在 C++ 中
const int32&
是否有默认参数,在蓝图中,都必须以引用的方式,传递一个参数到这个函数中。
(2)int32&
这个可以说是最迷惑人的函数传递方式,当你兴致勃勃地这么写函数参数时
UFUNCTION(BlueprintCallable)
void test1_5(int32& number)
{
number++;
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
你期望的结果是
但实际结果是,这个参数直接作为函数的返回值了,而并非你想要的上面的那种引用传递。在下面一小节会介绍 如何真正引用传递函数参数 的方式。
无论如何,我们先来看输出结果
可见,因为我们没有输入默认函数参数,所以 UE4 给他设定了一个初始值 0,这个道理同上面 int32
作为参数的情况。
而且由于 C++ 的语法(这时候 UE4 又讲究 C++ 的语法了),你无法给一个非常量引用类型的变量赋予一个右值,所以你也无法赋予其默认参数(在 VS 中就会自动报错)
这种方式的最大作用,在于可以让我们返回多个参数。同样在标准 C++ 中,也经常这么使用引用能让我们达到返回多个函数参数的目的
UFUNCTION(BlueprintCallable)
void test1_6(int32& number1, FString& str1, FMyTest& test1)
{
}
(3)神技之 UPARAM(ref) int32&
既然无法用 int32&
来达到引用传递函数参数的目的,那么如何才能做到这点。
此时 UE4 就特定为我们准备了一个宏 UPARAM(ref)
UFUNCTION(BlueprintCallable)
void test1_7(UPARAM(ref)int32& number1)
{
number1++;
}
测试一下
可以看到,此时,MyNumber
的值由原来的 0 变成了 1 ,也就是说,真正实现了 C++ 的引用传递。
(4)int32&&
在 C++11 中,可以通过右值引用,达到更省时省力的效果,但很可惜,只能在 C++ 内部使用右值引用,无法将右值引用类型的函数参数暴露给蓝图。
UFUNCTION(BlueprintCallable)
void test1_8(int32&& number)
{
number++;
}
编译就发生失败,可见右值引用无法传递给蓝图
三、指针传递函数参数在蓝图中的表现形式
由于引用的便捷性,我相信应该不会有要写 int32 *
这种需求的参数类型的函数吧。。。(有的话,麻烦告知我一声,而且编译时也不会通过)
个人认为一般就是传递多态的参数的时候需要用到指针,由于本人对于 UE4 智能指针的使用还没用过几次,而且暴露给蓝图的指针类型,基本都是原生指针,所以在这里不对智能指针的情况进行分析。
由于 UE4 所有需要带有反射类型的类,都必须继承自 UObject
,所以此处以 UObject
为例。并且 UObject
作为参数传递给蓝图使用时,只能用指针的形式。
并且注意,任何指针,都要注意其是否可能为空指针的情况。
(1)UObject*
同 C++ 的指针,可以更改传入的内容。地址传递。
UFUNCTION(BlueprintCallable)
void test2_1(UObject* MyObject, FString& ObjectName)
{
if (MyObject != nullptr)
{
ObjectName = MyObject->GetName();//获取object的名字
}
}
在蓝图中为
测试一下,由于 Actor
是继承于 UObject
的,所以可以将子类作为参数,传入父类的指针或引用中,实现动态多态。所以下面这个函数可以正常执行。
(2)const UObject*
这样传进来的 UObject
的值就不能发生更改。即 *UObject
的值不能发生改变。
UFUNCTION(BlueprintCallable)
void test2_2(const UObject* MyObject, FString& ObjectName)
{
if (MyObject != nullptr)
{
ObjectName = MyObject->GetName();
}
}
同样,在蓝图中的表现形式为
(3)const UObject*const
由于 const UObject
只是底层 const
,即不能修改 UObject
的值,可以修改它的地址。针对上面的函数,如果你加一句
MyObject++;
这样可行的,不过肯定会在运行时报错。
而在 C++,有这样一种 const
写法。即 const UObject*const
,底层+顶层 const
这样,你就无法用 MyObject++
了。
这样就能限定 MyObject
不被修改了。
UFUNCTION(BlueprintCallable)
void test2_3(const UObject*const MyObject, FString& ObjectName)
{
if (MyObject != nullptr)
{
ObjectName = MyObject->GetName();
}
}
在蓝图中为
在 C++ 中,当你既想传入原本的这个参数,又不想改变其地址的时候,下式成立
const UObject*const=const UObject&
但因为 UE4 的 UObject
只能以指针的形式传给蓝图。所以,这样写不成立。
UFUNCTION(BlueprintCallable)
void test2_3(const UObject& MyObject, FString& ObjectName)
{
}
(4)UObject*&
在学习数据结构时,特别是写树的时候,经常用到指针引用,来进行节点的连接和取消连接。
在 UE4 中,同样可以使用指针引用,来让传进来的指针的值和地址都会由于函数中的某些步骤而导致变化时,原来的实参的值和地址也会变化。
但由于 UE4 的引用的特殊性,按下面的方式写的话
UFUNCTION(BlueprintCallable)
void test2_4(UObject*& MyObject, FString& ObjectName)
{
if (MyObject != nullptr)
{
ObjectName = MyObject->GetName();
}
}
没错,就是作为返回值了
此时,同样也可以用之前的 UPARAM(ref)
来解决这个问题
UFUNCTION(BlueprintCallable)
void test2_5(UPARAM(Ref)UObject*& MyObject, FString& ObjectName)
{
if (MyObject != nullptr)
{
ObjectName = MyObject->GetName();
}
}
但此时传入的 UObject
必须为可写的对象,不可为只读对象
比如传入 self
时,会报错
所以这种类型参数的使用,只能用于可写的 UObject
对象。
(5)const UObject*&
无法用于蓝图,只能 C++ 中这么使用
UFUNCTION(BlueprintCallable)
void test2_6(const UObject*& MyObject)
{
}
四、函数参数类型为数组(TArray)时
- (1)TArray<int32>
- (2)const TArray<int32>
- (3)TArray<int32>&
- (4)const TArray<int32>&
UFUNCTION(BlueprintCallable)
void test3_1(TArray<int32> array1, const TArray<int32> array2, TArray<int32>& array3, const TArray<int32>& array4)
{
array1.Add(1);
UE_LOG(LogTemp, Warning, TEXT("array1 : %d"), array1.Last());
//array2 cannot be changed.
UE_LOG(LogTemp, Warning, TEXT("array2 : %d"), array2.Last());
array3.Add(3);
UE_LOG(LogTemp, Warning, TEXT("array3 : %d"), array3.Last());
UE_LOG(LogTemp, Warning, TEXT("array4 : %d"), array4.Last());
}
并且,除了 TArray<int32>&
对应的参数以外,其他所有参数,都必须连接一个参数,才能在蓝图中使用。其规则完全跟单纯 int32
的时候相同。
不连接的话
连接之后
- (5)TArray<UObject*>
- (6)const TArray<UObject*>
- (7)TArray<UObject*>&
- (8)const TArray<UObject*>&
当容器中的对象类型仅为
UObject*
时,其表现方式跟int32
一样
UFUNCTION(BlueprintCallable)
void test3_2(TArray<UObject*> array1, const TArray<UObject*> array2, TArray<UObject*>& array3, const TArray<UObject*>& array4)
{
}
- (9)容器中对象类型为
const UObject*,const UObject*const和UObject*&
时
UFUNCTION(BlueprintCallable)
void test3_3(TArray<const UObject*> array1)
{
}
UFUNCTION(BlueprintCallable)
void test3_4(TArray<const UObject*const> array1)
{
}
UFUNCTION(BlueprintCallable)
void test3_5(TArray<UObject*&> array1)
{
}
前面两个原因好像是不能将指针转换成常量指针这种,后面一个,已经看不出错在哪了。所以结论就是,UE4 的 TArray
里面的对象,对于原生指针,只能用 UObejct*
这种方式,加 const
修饰的和加引用的指针,都无法作为 TArray
里面的对象。
五、函数的返回值在蓝图中的表现形式
(1)int32
UFUNCTION(BlueprintCallable)
int32 test4_1(int32 number)
{
return number;
}
与用 int32&
的形参不同的是,此时在蓝图中,会显示一个 return value
(2)const int32
UFUNCTION(BlueprintCallable)
const int32 test4_2(int32 number)
{
return number;
}
跟 int32
在蓝图中的表现形式一样。
(3)int32&
在 C++ 中,返回值为引用的函数,可以作为左值使用,但是在蓝图中,是没有左右值的概念的。
并且 int32&
作为返回值时,其返回的变量,绝对不能是局部变量。因为返回局部变量的引用在 C++ 中就是大忌。
因此,当你这么写时,肯定会报错
UFUNCTION(BlueprintCallable)
int32& test4_3(int32 number)
{
number++;
return number;
}
因此,我这边就用下面这个代码进行测试
UFUNCTION(BlueprintCallable)
int32& test4_3(UPARAM(ref)int32 &number)
{
number++;
return number;
}
(4)const int32&
UFUNCTION(BlueprintCallable)
const int32& test4_4(UPARAM(ref)int32& number)
{
number++;
return number;
}
大坑之返回值为 const int32
,int32&
,const int32&
。
在 C++ 中,如果你函数的返回值为 int32
,const int32
,int32&
,const int32&
。那么完全是不同的结果,但是在蓝图中,你会发现,无论你把返回值设为上面四种形式中的哪一种,它在蓝图中的结果,均为 int32
对应的结果。
首先我们写一个测试函数
UFUNCTION(BlueprintCallable)
void changeInt(UPARAM(ref)int32& number)
{
number+=100;
}
然后见证奇迹
a. const int32
在 C++ 中,如果你返回值是 const
,那么就代表返回值是一个右值,是无法作为一个左值使用的。即,你无法写出这样的代码。
但是在 UE4 中,我们的 test4_2
是这样的
UFUNCTION(BlueprintCallable)
const int32 test4_2(int32 number)
{
return number;
}
按 C++ 中的概念,其返回值是一个右值,是无法用于刚定义的 ChangeInt()
函数的。
但是在蓝图中,可以看到它竟然能连上去,并且能成功运行
当然,输出结果为 0 ,MyNumber
的值没有发生改变。
b.int32&
再来看看我们的 test4_3
,其返回值是 number
的引用,即在 C++ 中,要是对这个返回值进行修改,number
必然也会发生修改。
UFUNCTION(BlueprintCallable)
int32& test4_3(UPARAM(ref)int32 &number)
{
number++;
return number;
}
但是在蓝图中
结果还是 1,并没有加上 100
c.const int32&
看看最后的 test4_4
,这个返回值就根本不能被修改。
UFUNCTION(BlueprintCallable)
const int32& test4_4(UPARAM(ref)int32& number)
{
number++;
return number;
}
但是在蓝图中,可以看到其返回值成功传入了 ChangeInt
这个函数中,并且根据结果为 1 来看,这个返回值的类型也变成了 int32
.
d.结论
所以对于蓝图而言,返回值为常量或引用时,还是以值的方式返回。
这个结论同样适用于之前的参数为 int32&
的时候。如
UFUNCTION(BlueprintCallable)
void test1_5(int32& number)
{
number++;
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
在 C++ 中是引用传递,但是,实际上你在蓝图中,只能看到这个作为返回节点,并不能作为函数参数的输入节点。因此,还是以 int32
的方式返回。
(5)int32&&
无法通过蓝图返回右值引用
UFUNCTION(BlueprintCallable)
int32&& test4_5(int32 number)
{
number++;
return MoveTemp(number);
}
(6)UObject*
(偷个懒,我也懒得加指针判空了,用的时候注意加就好!)
就以当前创建的 Actor
对象为例
UFUNCTION(BlueprintCallable)
AMyActor* test4_6(UPARAM(ref)int32& number)
{
this->MyNumber = number + 10;
return this;
}
测试一下
可见,是可以返回指针的。并可以返回这个对象本身。还是比较有用。
其作用效果,跟在参数列表中写 UObject*
类型的参数是一致的
UFUNCTION(BlueprintCallable)
void test4_6_1(int32 number,AMyActor*MyActor)
{
this->MyNumber = number + 10;
MyActor = this;
}
但在蓝图中是会变成这样,造成视觉上的误解。但无论是 test4_6
的返回值还是 MyActor
传入 test4_6_1
后的值,都是 this
。
(7)const UObject*
无法作为返回值被蓝图调用,但在 C++ 中可以正常使用
UFUNCTION(BlueprintCallable)
const AMyActor* test4_7(const AMyActor*MyActor)
{
return MyActor;
}
(8)UObject*&
可以作为返回值被蓝图调用
UFUNCTION(BlueprintCallable)
AMyActor*& test4_8(UPARAM(ref)AMyActor*& MyActor)
{
return MyActor;
}
这种方式的返回方式,跟在函数参数列表中的 UObject*&
方式在蓝图中的表现方式一致,并且意义也相同。
通过下面这种方式也能实现一样的效果,但注意要传指针引用才能在蓝图中表现出一样的效果。
UFUNCTION(BlueprintCallable)
void test4_8_1(AMyActor*& MyActor)
{
}
如果想要将 MyActor
变为参数,加个 UPARAM(ref)
即 ok。(见 test2_5
)
注意:返回指针和返回指针引用,在 C++ 完全是两回事,但是一旦用于蓝图,可能返回的均为指针(在蓝图中表现为引用的形式)。
因找不到很好的测试案例,望有心人可以提供一下测试的思路。但鉴于上面对返回类型为
int32&
的分析结果( C++ 中返回类型为int32&
,但在蓝图中被 UE4 改成了int32
)。所以,此处假设返回值为指针引用时,其仍然跟返回指针的效果一致。
(9)TArray<int32>
此处用到了 Initializer_list
的方式进行初始化容器。
UFUNCTION(BlueprintCallable)
TArray<int32> test4_9()
{
return TArray<int32>{1,2,3,4,5,6,7,8};
}
在蓝图中
(10)const TArray<int32>
首先我们先在 C++ 中写一个改变容器内的数值的函数,让容器内每一个数的数值加 10
UFUNCTION(BlueprintCallable)
void changeArray(UPARAM(ref)TArray<int32>& array1)
{
for (auto& value:array1)
{
value += 10;
}
}
然后写出这个函数
UFUNCTION(BlueprintCallable)
const TArray<int32> test4_10()
{
return TArray<int32>{1, 2, 3, 4, 5, 6, 7, 8};
}
在蓝图中,表现为
想必已经猜到结果了,没错,跟之前 const int32
做返回值一致,其返回值也被从 const TArray<int32>
改成了 TArray<int32>
。所以当然可以连上 ChangeArray
(11)TArray<int32>&
UFUNCTION(BlueprintCallable)
TArray<int32>& test4_11(UPARAM(ref)TArray<int32>& array1)
{
return array1;
}
并在蓝图中,创建一个数组
测试一下
当然,结果一定为 0 1 2,而不是 10 11 12,因为返回值被从 TArray<int32>&
改成了 TArray<int32>
。
(12)const TArray<int32>&
UFUNCTION(BlueprintCallable)
const TArray<int32>& test4_12(UPARAM(ref)TArray<int32>& array1)
{
return array1;
}
测试一下
同样,返回值由 const TArray<int32>&
被改成了 TArray<int32>
六、关于函数重载
在 C++ 中,支持函数的重载,但暴露给蓝图的函数不支持函数重载。
UFUNCTION(BlueprintCallable)
void test1_1(int32 number)
{
number++;
}
UFUNCTION(BlueprintCallable)
void test1_1(int32 number,int32 b)
{
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
显示两个函数发生冲突。
如果想支持重载,只能在 C++ 内部用,而不暴露给蓝图。将 UFUNCTION
反射去掉就可以支持 C++ 内部的函数重载
UFUNCTION(BlueprintCallable)
void test1_1(int32 number)
{
number++;
}
//UFUNCTION(BlueprintCallable)
void test1_1(int32 number,int32 b)
{
UE_LOG(LogTemp, Warning, TEXT("%d"), number);
}
七、蓝图中能实现函数参数传递形式
1. 能设定默认值的基本数据类型
像 bool
,FString
,int32
,int64
等基本数据类型,是能在蓝图内设定其默认值的。
2. 无法设定默认值的数据类型
大多数类型,都在蓝图内无法设定默认值,只能在 C++ 中默认构造函数中设定默认值,或者在只用于 C++ 的函数中用默认参数,比如F DateTime
,FTransform
等等。
3. 值传递形式
有个很好的方式区分值传递和引用传递,当为值传递时,参数符号为这样
4. 引用传递形式
当改成引用传递后,变成了菱形
5. 在蓝图中没有 const 传递的形式,只能在 C++ 中设定这样的函数
6. 返回值均为指针(但是表现为引用类型)或者正常的类型,不带有引用和 const 属性
八、总结
(一)函数参数
1. 值传递(第一部分)
int32
const int32
此部分还有对默认函数参数的分析。
2. 引用传递(第二部分)
const int32&
;int32&
;- 神技之
UPARAM(ref) int32&
; int32&&
(不能用于蓝图)
3. 指针传递(第三部分)
UObject*
;const UObject*
;const UObject*const
;UObject*&
;UPARAM(Ref)UObject*&
;const UObject*&
(不能用于蓝图)
4. 容器方式传递(第四部分)
- (1)
TArray<int32>
- (2)
const TArray<int32>
- (3)
TArray<int32>&
- (4)
const TArray<int32>&
- (5)
TArray<UObject*>
- (6)
const TArray<UObject*>
- (7)
TArray<UObject*>&
- (8)
const TArray<UObject*>&
- (9)容器中对象类型为
const UObject*
,const UObject*const
和UObject*&
时,无法放在TArray
容器中。
(二)返回值(第五部分)
int32
;const int32
;int32&
;const int32&
;
(1-4 的返回类型均被蓝图改为 int32
,详见上述分析--大坑之返回值为 const int32
,int32&
,const int32&
。)
-
int32&&
;(无法用于蓝图) -
UObject*
; -
const UObject*
;(无法用于蓝图) -
UObject*&
; (6,8 的返回类型应该都是UObject*
,详见上述分析,但有待核实) -
TArray<int32>
; -
const TArray<int32>
; -
TArray<int32>&
; -
const TArray<int32>&
(9-12 的返回类型均为TArray<int32>
,详见上述分析)
(一)和(二)结论
在 C++ 的返回值和函数参数中。如果在蓝图中,对应的参数为蓝图中的返回值,其中的const
和&
属性都被消除,但是若对应的参数为蓝图中的函数参数,其const
和&
属性都被保留。
(三)其他(六-七部分)
1. 关于函数重载
蓝图无法实现,只能 C++ 实现
2. 关于蓝图中能实现的函数参数传递形式,详见第七部分
本只打算自己总结下,没想到扩展了这么多,也看到了一些坑,如有遗漏,欢迎补充,要是你觉得有用,可以赏个赞。