跳转至

函数

约 1672 个字 119 行代码 预计阅读时间 7 分钟

函数签名

函数签名包括函数名,参数类型,参数个数和顺序, 是一个函数的标识符. 在链接之前时, 不同的编译器会根据函数签名给出函数的名称修饰(符号修饰,mangled name). 这个修饰指明了在重载的情况下,具体调用哪个函数.

Note

如何查看函数签名?

__func__, 在C99以及C++11之后的标准中存在, 其类型是const char字符数组, 返回值是函数名称

#include <string>
#include <iostream>

namespace Test
{
    struct Foo
    {
        static void DoSomething(int i, std::string s)
        {
        std::cout << __func__ << std::endl; //(1)!
    };
}

int main()
{
    Test::Foo::DoSomething(42, "Hello");

    return 0;
}
  1. 输出

    DoSomething
    

___FUNCTION__, 非标准但是广泛支持, 在__func__出现前就在C和C++中被各家编译器使用, 直到C99和C++11引入__func__. 实际效果等同于__func__.

__FUNCSIG__, MSVC平台, 输出函数签名, 包含模板信息.

__FUNCDNAME__, MSVC平台, 输出函数修饰名, 是编译器链接所使用的名字.

void exampleFunction()
{
    printf("Function name: %s\n", __FUNCTION__);
    printf("Decorated function name: %s\n", __FUNCDNAME__); //(1)!
    printf("Function signature: %s\n", __FUNCSIG__)
}
  1. Function name: exampleFunction
    Decorated function name: ?exampleFunction@@YAXXZ
    Function signature: void __cdecl exampleFunction(void)
    

__PRETTY_FUNCTION__, GCC/Clang, 类似于__FUNCSIG__, 包含模板信息.

template <typename T>
void Foo::bar(T x) {
    std::cout << __PRETTY_FUNCTION__ << "\n";//(1)!
}
  1. void Foo::bar(T) [with T = int]
    

对于跨平台需求, 使用条件编译来根据当前平台类型来把上述宏包装为一个自己的宏.

函数的重载

C语言中,函数签名不包括参数类型和顺序,所以不能重载同名但参数列表不同的函数.

C++中,函数签名包括参数类型和顺序,可以重载同名而参数类型/个数/顺序不同的函数.注意函数签名一般不包括返回值类型,因为返回值并不总是被使用,如果两个函数只有返回值类型不同且调用该函数后的返回值未被使用,则编译器缺乏信息推断究竟应调用哪个重载函数.

函数重载遇到默认参数时, 可能产生二义性(见后).

函数的默认参数

  • 函数的默认参数只能放在参数列表末尾
  • 若函数声明和定义分开,默认参数只能在声明中定义
void func_1(int val_1 = 10)
{
    cout << val_1 << endl;
}
 
void func_2(int val_1 ,int val_2 = 10);
 
int main(int argc, char const *argv[])
{
    func_1();
    func_2(10);
    
    return 0;
}
 
void func_2(int val_1 ,int val_2)func_1();
{
    cout << val_1 << endl;
}

有默认参数的函数可能引起的二义性:

#include <iostream>
using namespace std;
 
void func2(int a, int b = 10) {
    cout << "func2(int a, int b = 10) 调用" << endl;  // 带默认参数的函数
}
 
void func2(int a) {
    cout << "func2(int a) 调用" << endl;  // 无默认参数的函数
}
 
int main() {
    // func2(10);  // 这行代码会产生二义性
    func2(10, 20);  // 直接传递两个参数,明确调用两个参数的版本
    return 0;
}

函数与数组作为参数

借由指针实现的数组作为参数详见C语言复习; 简而言之, 数组会先变成指向数组的首元素的指针再传入, 同时形参声明的数组类型也会被视为指向数组元素的指针类型.

而传递对数组的引用, 在函数内部仍被视为数组.

Note
#include <iostream>
using namespace std;

void TestArrPtr(int *datas){
    cout << "TestArrPtr " << sizeof datas << endl;
}

void TestArrRef(int (&datas)[10]){
    cout << "TestArrRef " << sizeof datas << endl;
}

int main(){
    int datas[10] = {1, 2, 3, 4, 5};
    TestArrRef(datas);
    TestArrPtr(datas);
} 

输出为(x64平台):

TestArrRef 40
TestArrPtr 8

哑元函数

int fun(int,int a){   
     return a/10*10;
}
......
fun(1,5);//参数1不起作用, 但必须有

详见哑元与运算符重载:前缀++与后缀++

前置的++ (++i) 重载函数没有参数,返回对this对象的引用 后置的++ (i++) 重载函数有哑元, 编译器会让一个额外的0参与重载解析, i++调用的实际是i.operator ++(0)或者operator ++(i, 0). 后置++内部会拷贝自增之前的this对象给一个新对象并按值返回新对象, 体现为后置的++返回自增之前的值.

函数与容器作为参数, 以及初识移动语义

容器作为参数, 有引用和按值传递.

按值传参

即使按值传递, 对于容器C++有一些自动优化:

#include <iostream>
#include <vector>
using namespace std;

vector<int> testVector(vector<int> datas){
   cout << "testVector, datas"<<datas.data()<<endl;
   return datas;
}

int main(){
    vector<int> datas{1,2,3,4,5,6};

    cout<< "main, datas"<<datas.data()<<endl;
    auto datas1 = testVector(datas);
    cout<< "main, datas1" << datas1.data()<<endl;
}

.data()方法获得了容器的堆空间的首地址. 这段代码会输出三个地址,其中第二个地址和第三个地址相同,而第一个地址与第二第三个地址不同。因为在返回时, 如果 return 语句中的表达式是函数体中的一个 implicitly movable entity,并且它的类型和返回类型相同(忽略 cv), 则编译器首先尝试以移动的方式返回,即尝试先将表达式视为 rvalue;如果重载解析失败,再将表达式视为 lvalue 尝试以拷贝的方式返回。如果仍然失败,则编译错误。 而implicitly movable entity 是 具有 automatic storage duration 的一个 non-volatile object(局部变量datas属于此类), 或者此类object的右值引用. 所以编译器首先尝试以移动的方式返回, 将局部对象datas所指的堆空间直接移动给了datas1(交换datas1和局部对象datas的_begin/_end/_cap指针, 见”容器”一节).

若使用move(), 还可以减少拷贝内存开销:

#include <iostream>
#include <vector>
using namespace std;

vector<int> testVector(vector<int> datas){
    cout << "testVector, datas " << datas.data() << endl;
    return datas;
}

int main(){
    vector<int> datas{1,2,3,4,5,6};

    cout << "main, datas " << datas.data() << endl;
    auto datas1 = testVector(move(datas)); 
    cout << "main, datas1 " << datas1.data() << endl;
}
上方代码输出三个相同的地址. move()将表达式强制转换成右值(xvalue), 告诉编译器将main函数的datas对象当成一个”可以被偷资源的临时对象”, 则后续调用的构造函数/赋值运算符会优先匹配移动构造/移动赋值.

所以在传参之后, testVector函数使用了移动构造, 让作为形参的datas抢走了实参datas的内部指针_begin/_end/_cap. 之后, 同样地, 在返回时通过移动构造返回值来把这块堆空间交给了datas1.

事实上, 除了上述解释所涉及的, 上述代码的优化还包括对象被同类型prvalue初始化时copy elision的作用: 在返回时理应先在testVector(datas)处构造一个临时对象ret, 而后再使用ret移动构造给datas1(这里是初始化datas1, 所以是移动构造不是移动复制). Copy elision的作用是不构造临时对象ret, 而直接构造datas1, 节省了一次移动构造. 不过忽略copy elision并不影响解释地址相同的现象.

按引用传参

再看这个例子:

#include <iostream>
#include <vector>
using namespace std;

vector<int> testVector(vector<int>& datas){
    cout << "testVector, datas " << datas.data() << endl;
    return datas;
}

int main(){
    vector<int> datas{1,2,3,4,5,6};

    cout << "main, datas " << datas.data() << endl;
    auto datas1 = testVector(datas); 
    cout << "main, datas1 " << datas1.data() << endl;
}

此时, 上方代码输出的3个地址, 第一个与第二个相同, 而第三个与第一二个不同. 所以若返回值类型不是引用类型, 而我们返回了一个引用, 移动构造反而不会被自动调用, 拷贝构造会被调用.

其原因是, 当返回类型为按值返回时, 我们返回一个局部对象, 编译器清楚函数内的局部对象将被销毁, 偷走这个局部对象的资源是没有影响的, 所以可以放心将局部对象的资源移动出来. 但若我们返回一个左值引用, 编译器不能推导被引用对象的资源是否是可移动的. 从语义上, 当我们传入一个引用类型, 我们只是期待让对象在函数内被修改或者仅仅只是为了高效, 我们通常后续还要使用这个对象, 而并不期望销毁它. 从语法上,左值引用类型不符合初始化返回值时的隐式移动规则(引用类型不是对象类型object type, 也不是局部变量的右值引用), 所以不能使用移动构造.

评论