C++函数参数传递

引用传递和值传递

1. 值传递

当形参是非引用类型时,实参的值会被拷贝给形参,实参和形参是两个完全不同的对象,函数对形参做的所有操作都不会影响实参。

Tips:当形参是指针类型时,形参和实参也是两个完全不同的指针,只不过他们指向同个对象。因为指针使我们可以间接地访问它所指向的对象,因此通过指针可以修改它所指对象的值。

熟悉C语言的程序员常常使用指针类型的形参访问函数外部的对象,在C++语言中,建议使用引用类型的形参替代指针。

2. 引用传递

Tips:如果函数无须改变引用形参的值,那么最好将其声明为常量引用。

当形参是引用类型时,我们说它对应的实参被引用传递,使用引用传递的原因在于:

  • 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型和数组等)根本就不支持拷贝操作,因此只能通过引用形参来访问该类型的对象
  • 引用形参可以实现多返回值(当然指针形参也可以实现这个功能)

const形参和实参

1. 忽略形参的顶层const

Tips:常量引用和非常量引用参数是可以重载的,因为这个时候是底层const而非顶层const。

当用实参初始化形参时会忽略形参的顶层const,即当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:

// 既可以给fcn传入const int, 也可以传入int
void fcn(const int i);

// 错误: fcn(const int i)忽略了顶层const, 相当于重复定义了fcn(int)
void fcn(int i);

2. 指针或引用形参与const

前面提到顶层const是不可以实现重载的,因为实参初始化形参时会忽略掉顶层const。由于我们可以用非常量初始化一个底层const对象,但是反过来不行,因此常量引用和非常量引用是可以重载的。

3. 形参尽量使用常量引用

Tips:一个普通的引用必须用同类型的对象初始化,我们不能将需要类型转换的对象传递给普通的引用形参。

把函数不会改变的形参定义成普通的引用是一种比较常见的错误,这么做给函数的调用者一种误导,即函数可以修改它的实参的值。另外使用引用而非常量引用也会极大地限制函数所能接受的实参类型(普通引用形参无法接受const对象、字面值或者需要类型转换的对象)。

数组形参

1. 传递数组形参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响:

  • 不允许拷贝数组:无法以值传递的方式使用数组参数
  • 使用数组时会将其转换成指针:当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针

尽管不能以值传递的方式传递数组,但是我们把形参写成类似数组的形式:

// 下面三个函数等价, 都接受const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);  // 这里的维度表示我们期望数组含有多少个元素, 实际上不一定

2. 传递数组形参大小

由于数组是以指针的形式传递给函数的,所以函数并不知道数组的确切尺寸,调用者一般需要提供一些额外的信息。管理数组形参通常有三种技术:

2.1 数组中包含结束标记(一般只有C风格字符串)

第一种方法要求数组本身包含一个结束标记,最典型的例子是C风格字符串,接受C风格字符串的函数在遇到空字符时就会停止:

void print(const char *cp) {
    if (cp) {               // cp不是空指针
        while (*cp) {       // 指针所指字符不是空字符
            cout << *cp++;  // 输出当前字符并将指针向前移动一个位置
        }
    }
}
2.2 使用标准库规范

Tips:标准库begin和end函数可以返回数组的首元素指针和尾后元素指针。

第二种方式是传递指向数组首元素和尾后元素的指针:

void print(const int *beg, const int *end) {
    while (beg != end) {
        cout << *beg++ << endl;
    }
}

int j[2] = {0, 1};
print(begin(j), end(j));
2.3 显式传递一个表示数组大小的形参

第三种方法是专门定义一个表示数组大小的形参:

// const int ia[]等价于const int *ia
// size表示数组的大小
void print(const int ia[], size_t size) {
    for (size_t i = 0; i != size; ++i) {
        cout << ia[i] << endl;
    }
}

int j[] = {0, 1};
print(j, end(j) - begin(j));

3. 数组形参与const

当函数不需要对数组元素执行写操作时,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向常量的指针。

4. 数组引用形参

Tips:当形参是数组的引用时,维度也是类型的一部分。

C++语言允许将变量定义为数组的引用:

// 形参是数组的引用, 维度是类型的一部分
void print(int (&arr)[10]) {
    for (auto elem : arr) {
        cout << elem << endl;
    }
}

注意arr两边的括号是必不可少的:

f(int &arr[10]);    // 错误: 将arr声明成了引用的数组
f(int (&arr)[10]);  // 正确: arr是具有10个整数的整型数组的引用

由于数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内我们可以放心地使用数组。但是这一用法也无形中限制了print函数的可用性,我们只能将函数作用于维度为10的数组。

5. 传递多维数组

前面我们提到过C++中并没有真正的多维数组,所谓的数组其实是数组的数组。和所有的数组一样,当我们把多维数组传递给函数时,实际上传递的是指向数组首元素的指针,即一个指向数组的指针。

Tips:由于数组第二维以及后面的维度的大小都是数组类型的一部分,因此传递多维数组时不能省略。

// matrix是指向含有10个整数的数组的指针
void print(int (*matrix)[10], int rowSize);

// 等价定义
// 由于编译器会忽略掉第一个维度, 因此最好不要把它包含在形参列表内
void print(int matrix[][10], int rowSize);

main函数处理命令行选项

假设我们的可执行文件名为prog,我们可以向程序传递如下选项:

prog -d -o ofile data0

这些选项会通过两个可选的形参传递给main函数:

int main(int argc, char *argv[]);

// 等价
int main(int argc, char** argv);

其中第二个形参argv是一个数组,它的元素是指向C风格字符串的指针,第一个形参argc表示数组中字符串的数量。

当实参传递给main函数之后,argv第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。

在前面的例子中,argc等于5,argv指向的类型如下:

argv[0] = "prog";  // 或者一个空字符串
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;

可变形参

1. 支持可变形参的三种方法

有时候我们无法知道应该向函数提供几个实参,为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:

  • 如果所有的实参类型相同,传递名为initializer_list的标准库类型
  • 如果实参的类型不同,可以编写可变参数模板(TODO:p618页介绍)

C++还提供了一种特殊的形参类型(即省略符),可以用于传递可变数量的实参,不过这种功能一般只用于与C函数交互的接口程序。

2. initializer_list形参

Tips:initializer_list对象中的元素永远都是常量。

如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。和vector一样,initializer_list也是一种模板类型,但是initializer_list对象中的元素永远都是常量值,我们是无法改变的。

#include <initializer_list>
#include <string>
#include <iostream>

void print(std::initializer_list<std::string> list) {
    for (auto it = list.begin(); it != list.end(); ++it) {
        std::cout << *it << std::endl;
    }
}

int main() {
    print({"tomo", "cat", "tomocat"});
}


3. 省略符形参

Tips:省略符形参只能出现在形参列表的最后一个位置,并且仅仅用于C和C++通用的类型。

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。

4. 可变参数函数模板

可变参数函数模板指的是接收可变数目参数的模板函数。可变数目的参数被称为参数包,包括两种参数包:

  • 模板参数包:表示零个或多个模板参数
  • 函数参数包:表示零个或多个函数参数
// Args: 模板参数包
// rest: 函数参数包
template <typename T, typename... Args>
void foo(const T &t, const Args&... rest);
4.1 sizeof...运算符

我们可以使用sizeof...运算符来获取参数包中元素个数:

template <typename... Args> void bar(Args... args) {
    cout << sizeof...(Args) << endl;  // 类型参数的数目
    cout << sizeof...(args) << endl;  // 函数参数的数目
}
4.2 编写可变参数函数模板

Tips:可变参数函数模板通常是递归的。当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则可变参数版本会无限递归。

// 用来终止递归并打印最后一个元素的函数, 必须在可变参数版本的print定义之前声明
template <typename T>
ostream &print(ostream &os, const T &t) {
    return os << t;
}

// 包中除最后一个元素之外的其他元素都会调用这个版本的print
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) {
    os << t << ", ";
    return print(os, rest...);
}

Reference

[1] C++ Primer

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容