C++ Primer 总结索引 | 第六章:函数

1、函数是一个命名了的 代码块,通过 调用函数执行 相应的代码。函数 可以有零个 或 多个参数,通常 会产生一个结果
可以 重载函数,同一个名字可以对应 几个不同的函数

1、函数基础

1、一个典型的函数定义 包括:返回类型、函数名字、由0个或多个形参组成的列表 以及 函数体
其中函数体的括号 即使只有一句话 也不能省(跟if / for语句 不一样)double square (double x) return x * x;
需要有大括号(块):double square (double x) { return x * x; }

2、调用运算符 来执行函数。调用运算符的形式 是一对圆括号,它作用于 一个表达式,该表达式是 函数 或者 指向函数的指针;圆括号内 是一个用逗号隔开的 实参列表,用实参 初始化 函数的形参。调用表达式的类型 就是函数的返回类型

3、调用函数:函数的调用 完成两项工作:
1)用实参初始化函数 对应的形参
2)将控制权 转移给 调用函数,此时 主调函数的执行 被暂时中断,被调函数 开始执行

4、执行函数的第一步是 (隐式地)定义 并初始化它的形参。当调用fact函数时,首先 创建一个名为val的int变量,然后将它初始化为 调用时所用的实参5

// val的阶乘:val * (val - 1) * ... * 1
int fact(int val)
{
	int ret = 1;
	while (val > 1)
		ret *= val--;
	return ret;
}

调用函数

int main()
{
	int j = fact(5);
	return 0;
}

5、当遇到 return语句的时候 函数结束执行过程。return 语句完成了两项工作:
1)返回return语句 中的值
2)将控制权 从被调函数 转移回 主调函数。函数返回值用于初始化 调用表达式的结果,之后继续完成 调用所在表达式的剩余部分
对于上述fact() 相当于执行

int val = 5;
{ /* fact函数体内的代码 */ }
int j = ret; // 用jet的副本 初始化j

6、形参 和 实参:实参 是形参的初始值。尽管 实参和形参 存在对应关系,但是并没有规定 实参的求值顺序,编译器 可以按任意可行的顺序 对实参求值

实参和形参的区别:形参在函数的定义中声明;实参是形参的初始值

实参的类型 必须 跟对应的形参的类型匹配,因为在初始化过程中 初始值的类型 必须 跟初始化对象的类型 匹配
函数 有几个形参,就必须提供相等数量的 实参,所以 形参一定会 被初始化
对于fact函数,只有一个int类型的形参,每次调用的时候,必须提供一个 能转换成int的实参:

fact("hello"); // 错误:实参类型不正确,不能将const char* 转换成 int
fact(); // 错误:实参数量不一致
fact(3.14); // 正确:该实参 可以转换为int类型,执行调用时,实参隐式地转换成int类型(截去 小数部分)

7、函数的形参的列表:形参列表 可以为空,但是 不能省略
形参列表中的 形参通常用 逗号隔开,其中 每个形参都是含有 一个声明符的声明。即使 两个形参的类型一样,也必须把 两个类型都写出来

int f3(int v1, v2) { /* ... */ } // 错误
int f4(int v1, int v2) { /* ... */ } // 正确

任意两个形参 不能同名,而且 函数最外层作用域中的局部变量 也不能使用 与函数形参一样的名字

是否设置未命名的形参 并不影响调用时 提供的实参数量。即使 某个形参不被函数使用,也必须 为它提供一个实参

8、函数返回类型:函数的返回类型 不能是数组类型 或 函数类型,但是 可以是指向数组 或 函数的指针

1.1 局部对象

1、名字有作用域:详见2.4节,访问全局变量 局部变量,对象 有生命周期
1)名字的作用域 是程序文本的一部分,名字在其中可见
2)对象的生命周期 是程序执行过程中 该对象存在的 一段时间

2、函数体是 一个语句块,块构成一个 新的作用域,可以在其中 定义变量
形参 和 函数体内部定义的变量 统称为 局部变量。它们对函数而言 是“局部”的,仅在 函数的作用域内可见,同时 局部变量还会隐藏在外层作用域中 同名的其他所有声明中

在所有函数体之外定义的对象 存在于 程序整个执行过程中。此类对象 在程序启动时被创建,直到 程序结束时 才会销毁

3、局部变量的生命周期 依赖于 定义的方式

1)自动对象
对于 普通局部对象对应的对象 来说,当函数的控制路径 经过变量定义语句时 创建该对象,当 到达定义所在的块末尾时 销毁它
把只存在于 块执行期间的对象 称为 自动对象。当块的执行结束后,块中创建的自动对象的值 变成 未定义的

形参是 一种自动对象。函数开始时 为形参申请 存储空间,因为 形参定义 在函数体作用域之内,所以 一旦函数终止,形参 也就被销毁

用传递给函数的实参 初始化 形参对应的自动对象。对于 局部变量对应的自动对象来说,分为 两种情况
(1)如果 变量本身含有初始值,就用这个初始值 进行初始化
(2)如果 变量定义本身 不含有初始值,执行默认初始化(区别于 值初始化:都初始化为0),这意味着 内置类型的未初始化的局部变量将 产生未定义的值

附(第2章 2.1节 7):
默认初始化:定义变量时没有指定初始值。默认值是什么 由 变量类型 和 定义变量的位置 决定
内置类型的变量没被显式初始化,其值由定义的位置决定。定义于任何函数体之外的变量被初始化为0;定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,试图拷贝或以其他形式访问此类值将引发错误

2)局部静态变量
令 局部变量的生命周期 贯穿函数调用 及之后的时间。可以 将局部变量定义成 static类型 从而获得这样的对象

局部静态对象 在程序的执行路径 第一次经过 对象定义语句时 初始化,并且直到 程序中止才被销毁,在此期间 即使对象所在的函数 结束执行也不会 对它有影响
如:函数统计 它自己被调用了多少次

size_t count_calls()
{
	static size_t ctr = 0; // 函数调用结束后,这个值 仍然有效
	return ++ctr;
}
int main()
{
	for (size_t i = 0; i != 10; i++)
		cout << count_calls() << endl;
	return 0;
}

在控制流第一次经过 ctr 的定义之前,ctr 被创建 并初始化为0,每次调用将 ctr加1 并返回新值。每次执行 count_calls 函数时,变量 ctr 的值 都已经存在 并且 等于函数上一次退出时 ctr 的值
第二次 调用时ctr的值是1,以此类推

如果 局部静态变量没有显式的初始值,它将 执行值初始化,内置类型的局部静态变量 初始化为0

附(第3章 3.1节 5:vector的值初始化):
值初始化:只提供vector对象 容纳元素的数量 而不用略去 初始值。库会 创建一个 值初始化的元素初值,把它赋给 容器中的所有元素,初值由vector对象中的元素类型 决定

vector<int> ivec(10); // 10个元素,每个都初始化为0
vector<string> svec(10); // 10个元素,每个都是空的string对象

4、说明形参、局部变量以及局部静态变量的区别。编写一个函数,同时达到这三种形式
形参是局部变量的一种
形参和函数体内部定义的变量统称为局部变量
局部静态变量的生命周期贯穿函数调用及之后的时间

#include <iostream>

size_t count_add(int n)       // n形参,局部变量
{
    static size_t ctr = 0;    // ctr局部静态变量
    ctr += n;
    return ctr;
}

int main()
{
    for (size_t i = 1; i != 10; ++i)  // i局部变量
        std::cout << count_add(i) << std::endl;
    return 0;
}

1.2 函数声明(也称作 函数原型)

1、和其他名字一样,函数的名字 必须在使用前声明。类似于变量,函数 只能定义一次,但可以声明多次

2、函数的声明 和函数的定义 非常类似,所以 也就无需形参的名字

3、函数的三要素(返回类型、函数名、形参类型)描述了函数接口,说明了 调用函数所需的全部信息

4、在头文件中 进行函数声明:确保同一函数的所有声明 保持一致。一旦 想改变函数接口,只需改变一条声明即可
定义函数的源文件 应该把含有函数声明的头文件 包含进来,编译器负责验证 函数的定义 和 声明 是否匹配
如:编写一个名为Chapter6.h 的头文件,令其包含6.1节 fact()函数的声明

#ifndef CHAPTER6_H
#define CHAPTER6_H

int fact(int n);

#endif

1.3 分离式编译

1、希望把 程序的各个部分 分别存储在不同文件中。为了 允许编写程序时 按照逻辑关系 将其划分开来,C++语言支持 分离式编译。分离式编译 允许把程序分割到 几个文件中,每个文件 独立编译

2、编译 和 链接多个源文件:fact 函数位于一个 名为fact.cpp的文件中,其声明位于名为 Chapter6.h 的头文件中。与其他用到fact函数的文件一样,fact.cpp应该包含Chapter6.h头文件,另外 在名为factMain.cpp的文件中 创建main函数,main函数会调用 fact函数。要生成可执行文件 必须告诉编译器 用到的代码在哪里
书上示例

$ CC factMain.cpp fact.cpp # 生成factMain.exe 或者 a.out 默认名字
$ CC factMain.cpp fact.cpp -o main # 生成main 或者 main.exe 给了名字

实际编译的过程
实际编译过程
fact.cpp

// #include "Chapter6.h"

int fact(int val)
{
	int ret = 1;
	while (val > 1)
		ret *= val--;
	return ret;
}

factMain.cpp

#include "Chapter6.h"
#include <iostream>

int main()
{
	int j = fact(5);
	std::cout << j << std::endl;
	return 0;
}

Chapter6.h

#ifndef CHAPTER6_H
#define CHAPTER6_H

int fact(int n);

#endif

3、如果 修改了其中一个源文件,只需要 重新编译那个 改动了的文件。大多数编译器 提供了分离式编译 每个文件的机制,这一过程 通常会产生一个后缀名是.obj(windows)或.o(unix)的文件,后缀名的含义 是该文件包含对象代码
接下来 编译器负责 把对象文件链接在一起 形成可执行文件
编译 链接过程
四句话分别的作用是:生成 factMain.o;生成 fact.o;生成 a.exe(默认名字);运行 a.exe

2、参数传递

1、每次调用函数时 都会重新创建它的形参,并用 传入的实参 对形参进行初始化。形参的类型 决定了 形参和实参交互的方式。如果 形参是引用类型,它将绑定到 对应的实参上;否则 将实参的值 拷贝后 赋值给形参

2、当形参是 引用类型时,它对应的实参 被引用传递,或者 函数被 传引用调用。引用形参 是它对应的实参的别名
当实参的值 被拷贝给形参时,形参 和 实参 是两个独立的对象。这样的实参 被值传递 或者 函数被传值调用

2.1 传值参数

1、当 初始化一个 非引用类型的变量时,初始值被拷贝给 变量,此时 对变量的改动不会 影响初始值

int n = 0; // int类型的初始变量
int i = n; // i是n的值的副本
i = 42; // i的值改变,n的值不变

传值参数的机理完全一样,函数对形参 做的所有操作 都不会影响实参
例如

int fact(int val)
{
	int ret = 1;
	while (val > 1)
		ret *= val--;
	return ret;
}

尽管 fact函数 改变了val的值,但是 这个改变 不会影响传入fact的实参。调用 fact(i) 不会改变 i 的值

2、指针形参:指针的行为 和 其他非引用类型一样。当 执行指针拷贝操作时,拷贝的是 指针的值。拷贝之后,两个指针 是不同的指针。因为 指针使我们 可以间接地访问它所指的对象,所以 通过指针 可以修改它所指对象的值:

void reset(int *ip)
{
	*ip = 0; // 改变指针ip所指对象的值
	ip = 0; // 只改变了ip的局部拷贝,实参 未被改变
}

调用 set函数之后,实参所指的对象 被置为0,但是 实参本身 并没有改变
reset(&i); 改变i的值 而非i的地址

3、编写一个函数,使用指针形参交换两个整数的值
虽然 还是值传递,但是可以使用 解引用运算符 改变 相同位置存放的数值

#include <iostream>

void swap_int(int *i,int *j)
{
	int tmp = *i;
	*i = *j;
	*j = tmp;
}

int main()
{
	int i = 1,j = 2;

	std::cout << i << " " << j << std::endl;
	swap_int(&i,&j);
	std::cout << i << " " << j << std::endl;

	return 0;
}

2.2 传引用参数

1、对于 引用操作 实际上是作用在 引用所引的对象上。通过 使用引用形参,允许函数 改变 一个或多个实参的值

void reset(int &i) // i是传给reset函数的对象 的另一个名字
{
	i = 0; // 改变了i所引对象的值
}

2、调用这一版本的reset函数时,直接传入对象 而无需 传递对象的地址

int j = 42;
reset(j); // j采用传引用的方式,它的值 被改变
cout << "j = " << j << endl; // 输出 j = 0

形参i仅仅是j的 又一个名字。在reset内部 对i的使用即是对j的使用

3、使用引用 避免拷贝:拷贝大的类类型对象 或者 容器对象比较低效,甚至有的类型(包括IO类型在内)根本不支持 拷贝操作。当某种类型不支持拷贝操作时,函数只能通过 引用形参访问 该类型的对象

准备编写一个函数 比较两个string对象的长度,因为 string对象可能会非常长。因此 应该尽量避免 直接拷贝它们,这时 使用引用形参 是比较明智的选择。又因为 比较长度无需改变 string对象的内容,所以把 形参定义成 对常量的引用

bool isShorter(const string &s1, const string &s2)
{
	return s1.size() < s2.size();
}

如果 函数无需改变 引用形参的值,最好将其 声明为 常量引用

4、使用引用形参 返回额外信息:一个函数只能返回一个值,有的时候 需要同时返回多个值,引用形参 可以返回多个结果
如:定义函数 使它能够 既返回位置 也返回出现次数?
1)定义一个新的数据类型,让它包含位置 和 数量两个成员
2)更简单的:可以给函数传入 一个额外的引用实参,令其 保存字符出现的次数

// 返回s中c第一次出现的位置索引
// 引用形参occurs负责 统计c出现的总次数
string::size_type find_char(const string &s, char c, string::size_type &occurs)
{
	auto ret = s.size(); // 第一次出现的位置
	occurs = 0; // 设置表示出现次数的形参的值
	for (decltype(ret) i = 0; i != s.size(); ++i) {
		if (s[i] == c) {
			if (ret == s.size())
				ret = i;
			++occurs;
		}
	}
	return ret; // 出现次数通过 occurs隐式的返回
}

对于 string::size_type find_char(const string &s, char c, string::size_type &occurs)中的三个参数
为什么是现在的类型,特别说明为什么s是常量引用而occurs是普通引用?
为什么s和occurs是引用类型而c不是?
如果令s是普通引用会发生什么情况?
如果令occurs是常量引用会发生什么情况?

s不需要改变实参,occurs需要改变实参;
s可能会很大,occurs需要改变实参,c没有上述两个需求;
如果s是普通的引用可能改变实参;
occurs是常量引用则不能改变实参,++occurs会报错

调用函数 auto index = find_char(s, 'o', ctr); 如果string对象中 确实存在o,那么 ctr的值 就是 o出现的次数,index指向 o第一次出现的位置;否则如果string对象中没有o,index等于 s.size() 而ctr等于0

5、形参不能是引用类型的情况:很多函数的形参都是iterator的传值,而不是传引用?为什么不是传引用,传引用不是效率更高吗?

void print_vector(vector<int>::iterator beg, vector<int>::iterator end) {
	while(beg != end) { 
		cout << *beg++ << endl; 
	}
}

print_vector(v.begin(), v.end());

这里形参如果是引用的话,则编译时会报错,因为v.begin()返回的是一个临时变量,是一个 右值,它不能赋值给一个非const的引用
可以赋值给const 引用;即如果print_vector的形参是const的iterator,就可以传v.begin()给它。但这样一来,在函数体内就不能做++,–或任何修改该iterator的操作了

6、类似的道理,函数的返回值通常不能传给一个 非const的引用,因为函数返回值通常也是一个 临时变量,是一个右值

int f() { return 1; }
int &ri1 = f();    //error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
const int &ri2 = f();   // OK

2.3 const形参 和 实参

1、当形参是const时,注意 关于顶层const的讨论,顶层const作用于 对象本身

const int ci = 42; // 不能改变ci,const是顶层的。本身正确,初始化
int i = ci; // 正确:当拷贝ci时,忽略了顶层const
int * const p = &i; // p的const是顶层的,不能给p赋值。本身正确,初始化时 忽略了顶层const
*p = 0; // 正确:通过p改变对象的内容 是允许的,现在i变成了0

和 其他初始化过程 一样,当 用实参初始化形参的时候 会忽略掉顶层const。形参的顶层const被忽略了。当 形参有顶层const时,传给它常量对象 或者 非常量对象 都是可以的

void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ }

调用fcn函数时,既可以传入 const int 也可以 传入int

void fcn(const int i) {}
void fcn(int i) {} // 错误,重复定义了fcn(int)

C++允许 定义若干具有 相同名字的函数,不过 前提是不同函数的形参列表 应该有明显区别。因为 顶层const被 忽略掉了,所以 在上面的代码中 传入 两个fcn函数的参数 可以完全一样

2、指针 或 引用形参 与 const:形参的初始化方式 和 变量初始化方式一样
可以 使用非常量初始化 一个底层const对象,但是 反过来不行;同时 一个普通的引用 必须用 同类型的对象 初始化

int i = 42;
const int *cp = &i; // 正确:但是cp不能改变i
const int &r = i; // 正确:但是r不能改变i
const int &r2 = 42; // 正确
int *p = cp; // 错误:p的类型和cp的类型不匹配,静态没办法 赋值给 非静态
int &r3 = r; // 错误:r3的类型和r的类型不匹配
int &r4 = 42; // 错误:不能用 字面值 初始化一个非常量引用

将 同样的初始化规则 应用到 参数传递上可得如下形式

int i = 0;
const int ci = i;
string::size_type ctr = 0; 
reset(&i); // 调用 形参类型是int* 的reset函数
reset(&ci); // 错误:不能用指向const int对象的指针 初始化int*
reset(i); // 调用 形参类型是 int&的reset函数
reset(ci); // 错误:不能把普通引用 绑定到 const对象ci上
reset(42); // 错误:不能把普通引用 绑定在 字面值上
reset(ctr); // 错误:类型不匹配,ctr是无符号类型

// 正确:find_char的第一个形参是对常量的引用,该函数的引用形参是 常量引用,C++允许 用字面值初始化 常量引用
find_char("Hello World!", 'o', ctr);

想要调用 引用 版本的reset,只能使用int类型的对象,而不能使用 字面值、求值结果为int的表达式、需要转换的对象 或者 const int类型的对象
注意只有 引用才是这样,正常 都可以传入字面值
C++允许 用字面值初始化 常量引用

double calc(double)
calc(66);

int count(const string& char)
count("abc", 'a');

3、尽量使用常量引用:把函数不会改变的形参 定义为 (普通的)引用 是一种常见 错误,因为会给函数的调用者一种误导:函数可以修改其实参的值

此外,使用引用 而非常量引用 也会极大限制 函数所能接受的实参类型
因为 不能把 const对象、字面值 或者 需要类型转换的对象 传递给 普通的引用形参

以上面的count函数 为例,如果把形参 定义为 string&:int count(string&, char),调用 count("abc", 'a'); 将在编译时 发生错误
正确的修改的思路是 改正count函数的形参,实在不能修改,在调用该函数 的函数内部 定义一个string类型的变量,令其为s的副本,然后 把这个string对象 传递给count函数

4、引用形参什么时候应该是常量引用?如果形参应该是常量引用,而我们将其设为了普通引用,会发生什么情况?
当函数对参数所做的操作不同时,应该选择适当的参数类型。如果需要修改参数的内容,则将其设置为普通引用类型;否则,如果不需要对参数内容做任何更改,最好设为常量引用类型

就像前面几个练习题展示的那样,如果把一个本来应该是常量引用的形参设成了普通引用类型,有可能遇到几个问题:一是容易给使用者一种误导,即程序允许修改实参的内容;二是限制了该函数所能接受的实参类型,无法把 const 对象、字面值常量或者需要进行类型转换的对象传递给普通的引用形参

2.4 数组形参

1、两个 会对定义和使用 作用在数组上的函数有影响的 数组的两个特殊性质:
1)不允许拷贝数组
2)使用数组时 会将其转换为 指针

因为不能拷贝数组,所以 无法以值传递的方式 使用数组参数
因为数组会被转换为指针,所以 为函数传递一个数组时,实际上 传递的是 指向数组首元素的指针

2、尽管 不能以值传递的方式 传递数组,但是 可以把形参 写成 类似于数组的形式
尽管形式不同,但这三个 print函数是等价的
每个函数都有 一个 const int*类型的形参,当编译器 处理对print函数的调用时,只检查 传入的参数 是否是 const int*类型
引用形参就不能不写 数组维度(指针(const int*) 和 直接数组(const int[]) 都不用)

// 尽管形式不同,但这三个 print函数是等价的
// 每个函数都有 一个 const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 维度 表示期望数组含有多少元素,实际 不一定

传给print函数 的是一个数组,则 实参自动地转换为 指向数组首元素的指针,数组的大小 对函数的调用 没有影响
但是 以数组作为形参 的函数 也必须确保 使用数组时 不会越界

3、管理数组实参的方法
1)使用标记 指定数组长度 管理数组实参:要求数组本身 包含一个结束标记

使用这种方法典型例子 C风格字符串。C风格字符串 存储在 字符数组中,并且 在最后一个字符后面跟着 一个空字符。函数 在处理C风格字符串时 遇到空字符 停止

void print(const char *cp)
{
	if (cp) // 若cp不是一个空指针
		while (*cp) // 只要指针所指的字符 不是空字符
			cout << *cp++; // 输出当前字符 并将指针向前 移动一个位置
}

适用于 有明显结束标记 且该标记 不会与 普通数据 混淆的情况,但是 对于像int这样 所有取值 都是合法值的数据 就不太有效了

2)使用 标准库规范 管理数组实参:传递 指向数组首元素 和 尾后元素的指针,受到标准库技术的启发

void print(const int *beg, const int *end)
{
	// 输出beg到end之间的(不含end)的所有元素
	while (beg != end)
		cout << *beg++ << endl; // 输出当前元素 并将指针向前 移动一个位置
}

int j[2] = {0, 1}; // j转换为指向它 首元素的指针,j为int*型
print(begin(j), end(j));

3)显式传递 一个表示数组大小的形参
size表示数组大小,将它显式地传给函数 用于 控制对ia元素的访问

// const int ia[]等价于const int* ia
// size表示数组大小,将它显式地传给函数 用于 控制对ia元素的访问
void print(const int ia[], size_t size)
{
	for (size_t i = 0; i != size; ++i) {
		cout << ia[i] << endl;
	}
}

int j[] = {0, 1}; // 大小为2的整型数组
print(j, end(j) - begin(j));

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

5、数组引用实参:C++允许将变量 定义成 数组的引用,形参 也可以是数组的引用。此时 引用形参 绑定到 对应的实参上,也就是 绑定到数组上

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

&arr 两端的括号不能少

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

数组的大小 是构成 数组类型的一部分,这一用法限制了 print函数的可用性,只能将函数作用于 大小为10 的数组

int i = 0, j[2] = {0, 1};
int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(&i); // 错误:实参不是含有10个整数的数组
print(j); // 错误:实参不是含有10个整数的数组
print(k); // 正确:实参是含有10个整数的数组

6、传递 多维数组:C++没有真正的多维数组,其实是 数组的数组
和 所有数组一样,当 将多维数组传递给函数时,真正传递的是 指向数组首元素的指针。数组第二维(以及后面所有维度)的大小 都是数组类型的一部分,不能省略

// matrix指向数组的首元素,该数组的元素 是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) { /* ... */ }

// 等价定义
void print(int matrix[][10], int rowSize) { /* ... */ }

等价定义 使用数组的语法 定义函数,此时 编译器会一如既往地忽略掉 第一个维度,所以 最好不要把他包括在形参的列表内
matrix的声明看起来 是个二维数组,实际上 形参是指向含有 10个整数的数组的指针

matrix两端的括号 不能少:

int *matrix[10]; // 10个指针构成的数组
int (*matrix)[10]; // 指向含有10个整数的数组的指针

7、编写一个函数,令其交换两个int指针
swap(&i, &j); 不能这样直接取地址,因为会识别成引用
int* a = &i, * b = &j; // 注意b前面加*

#include <iostream>
using std::cout; using std::endl;

void swap(int*& a, int*& b) {
	int* tmp = b; // 要先给tmp赋值了 才能使用
	b = a;
	a = tmp;
}

int main()
{
	int i = 0, j = 0;
	cout << &i << " " << &j << endl;
	// swap(&i, &j); 不能这样直接取地址,因为会识别成引用
	int* a = &i, * b = &j; // 注意b前面加*
	swap(a, b);
	cout << a << " " << b << endl;
	return 0;
}

8、编写print函数,依次调用每个函数使其输入下面定义的i和j
常数引用,数组指针+大小,开始结束指针

#include <iostream>

using std::cout; using std::endl;
using std::begin; using std::end;

void print(const int(&j)[2]) { // 参数为常量数组引用,天然包含数组的大小的检查,不需要加 长度信息了
	for (int i : j)
		cout << i << " ";
	cout << endl;
}

void print(const int* beg, const int* end) { // 参数为开始结束 常量指针,两种循环写法
	for (const int* i = beg; i != end; ++i) {
		cout << *i << " ";
	}
	cout << endl;
	
	while (beg != end) {
		cout << *beg++ << " ";
	}
	cout << endl;
}

void print(const int j[], size_t size) { // 参数为数组指针+大小
	for (size_t i = 0; i != size; ++i) {
		cout << j[i] << " ";
	}
	cout << endl;
}

int main()
{
	int j[2] = { 0, 1 };
	print(j); // 常量数组引用
	print(j, 2); // 数组指针+大小
	print(begin(j), end(j)); // 开始结束 常量指针
	return 0;
}

9、如果代码中存在问题,请指出并改正

void print(const int ia[10])
{
	for (size_t i = 0; i != 10; ++i)
		cout << ia[i] << endl;
}

该函数传递的不是数组是const int*,如果实参不是含10个元素的int数组,可能导致for循环数组越界。修改为

void print(const int (&ia)[10]) { /*...*/ }

2.5 main:处理命令行选项

1、main函数 演示C++程序如何 向函数传递数组,用户通过设置一组选项 来确定函数 所要执行的操作

假定main函数位于 可执行文件prog之内,可以向程序传递 下面的选项:prog -d -o ofile data0
这些命令行选项 通过两个(可选)形参 传递给main函数:int main(int argc, char *argv[]) { ... }
第二个形参argv 是一个数组,它的元素是指向 C风格字符串的指针;第一个形参argc表示 数组中字符串的数量
因为 第二个形参是数组,所以main函数定义为:int main(int argc, char **argv) { ... } 其中 argv指向char*

当实参传给 main函数之后,argv的第一个元素 指向程序的名字 或者 一个空字符串,接下来的元素 依次传递命令行提供的 实参,最后一个指针之后的元素值 保证为0
所以,argc的值为5,argv应该包含 以下的C风格字符串:

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

2、当使用argv中的实参时,记得 可选的实参从argv[1]开始;argv[0] 保存程序名字,而非用户输入
例:编写一个main函数,令其接受两个实参。把实参的内容连接成一个string对象并输出出来
test.cpp

#include <iostream>

int main(int argc, char const* argv[])
{
	const std::string s1 = argv[1], s2 = argv[2];
	std::cout << s1 + s2 << std::endl;
	std::cout << argc << std::endl;
	for (size_t i = 0; i != 3; i++)
		std::cout << argv[i] << std::endl;;
	return 0;
}

输入及结果
输入及结果
先生成可执行文件(.exe),然后在命令行中输入

3、含有可变形参的函数:为了编写能处理 不同数量实参的函数,C++11提供两种主要方法
1)如果 所有的实参类型相同,可以传递 一个名为initializer_list的标准库类型
2)如果 实参的类型不同,可以编写一种特殊的函数,即 可变参数模板
C++还有 3)特殊的形参类型(省略符),可以用它传递 可变数量的实参。这种功能 只用于与C函数交互的接口程序

4、initializer_list形参:函数的实参数量未知 但是全部实参的类型都相同。initializer_list类型 定义在同名的头文件中
initializer_list 提供的操作:

代码作用
initializer_list<T> lst默认初始化;T类型元素的空列表
initializer_list<T> lst {a, b, c ...}lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst) / lst2 = lst拷贝或赋值一个 initializer_list对象 不会拷贝列表中的元素;拷贝后 原始列表和副本共享元素
lst.size()列表中的元素数量
lst.begin()返回指向lst中首元素的指针
lst.end()返回指向lst中尾元素 下一位置的指针

和 vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明 列表中所含元素的类型

initializer_list<string> ls; // 元素类型是string
initializer_list<int> li; // 元素类型是int

和vector不一样的是,initializer_list 对象中的元素 永远是常量值,无法改变 initializer_list对象中元素的值
例:用如下形式编写 输出错误信息的函数,使其可以 作用于 可变数量的形参

void error_msg(std::initializer_list<string> il) {
	for (auto beg = il.begin(); beg != il.end(); ++beg)
		cout << *beg << " ";
	cout << endl;
}

作用于 initializer_list 对象的begin 和 end操作 类似于vector对应的成员。begin()成员 提供一个指向列表首元素的指针,end()成员提供 一个指向列表尾后元素的指针

想向 initializer_list形参中 传递一个值的序列,则必须把序列放在 一对花括号内

// expected 和 actual 是string对象
if (expected != actual)
	error_msg({"functionX", expected, actual});
else
	error_msg({"functionX", "okay"});

调用了同一个函数error_msg,但两次传递的参数数量 不同

含有initializer_list 形参的函数 也可以拥有其他形参
下面的函数 包含一个initializer_list形参 和一个 ErrCode形参;elem为const std::string& 类型

void error_msg(ErrCode e, initializer_list<string> il)
{
	cout << e.msg() << ": ";
	for (const auto &elem : il)
		cout << elem << " ";
	cout << endl;
}

5、省略符形参:为了便于C++程序访问某些特殊的C代码设置,仅仅用于C和C++通用的类型,大多数类类型的对象在 传递给省略符形参时 都无法正确拷贝

省略符形参 只能出现在参数列表的最后一个位置,形式有两种

void foo(parm_list, ...);
void foo(...);

第一种形式 指定了foo函数的部分形参的类型,对应于这些形参的实参 将会执行正常的类型检查。省略符形参 所对应的形参 无需类型检查。在第一种形式中,形参后面的 逗号是可选的

6、编写一个函数,它的参数是initializer_list类型的对象,函数的功能是计算列表中所有元素的和
注意注释

#include <iostream>
#include <initializer_list>

using std::endl; using std::cout;

int getSum(std::initializer_list<int> il) {
	int res = 0;
	for (const int& i : il) // 一定记得加const,加不加引用 对结果无影响
		res += i;
	return res;
}

int main()
{
	std::initializer_list<int> il2 = { 1,2,3,4 }; 
	cout << getSum(il2) << endl;
	// 等价std::cout << counter_int({1,2,3,4,5}) << std::endl;
	return 0;
}

在范围for循环中使用initializer_list对象时,应该将循环控制变量声明成引用类型吗?
如果拷贝代价小,则无需设置成引用类型;如果拷贝代价大,可以设置成引用类型

3、返回类型 和 return语句

return语句终止当前正在执行的函数 并将控制权返回到 调用该函数的地方。return语句有两种形式

return;
return expression;

3.1 无返回值函数

1、没有返回值的return语句 只能用在返回值类型是 void的函数中。返回void的函数 不要求非得有return语句,这类函数最后一句后面会 隐式地执行return

2、void函数如果 想在它的中间位置 提前退出,可以使用return语句

3、一个 返回类型是void的函数 也能使用return语句的第二种形式,不过 此时return语句的expression必须是 另一个返回void的函数。强行令void函数返回其他类型的表达式将 产生编译错误

3.2 有返回值的函数

1、return语句 return expression; 提供了函数的结果。只要函数的返回类型 不是void,则 该函数内的 每条return语句 必须返回一个值。return语句返回值的类型 必须与函数的返回类型相同,或者能 隐式地转换成 函数的返回类型

2、在含有return语句的循环后面 应该也有一条return语句,如果 没有的话 程序就是错误的
即 控制流可能尚未返回任何值 就结束了函数的执行

// 检查 一个字符串 是否是 另一个字符串的子串
bool str_subrange(const string &str1, const string &str2)
{
	// 大小相同:此时用 普通的相等性判断结果 作为返回值
	if (str1.size() == str2.size())
		return str1 == str2;
	// 得到较短string对象的大小
	auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();
	// 检查两个string对象的对应字符 是否相等
	for (decltype(size) i = 0; i != size; ++i) {
		if (str1[i] != str2[i])
			return; // 错误1:没有返回值,编译器会报告
	}
	// 错误2:控制流可能尚未返回任何值 就结束了函数的执行,编译器可能检查不出这一错误
}

3、值的返回:返回值的方式 和 初始化一个变量 或 形参的方式 完全一样;返回的值 用于初始化调用点的一个临时量,该临时量 就是函数调用的结果

// 如果 ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr, const string &word, const string &ending)
{
	return (ctr > 1) ? word + ending : word;
}

该函数的返回类型是 string,意味着 返回值将被 拷贝到调用点。该函数 会返回一个word的副本 或者 未命名的临时string变量(word和ending的和)

如果 函数返回引用,则 该引用是 所引用对象的别名

// 挑出两个string对象中 较短的那个,返回其引用
const string &shorterString(const string &s1, const string &s2)
{
	return s1.size() <= s2.size() ? s1 : s2;
}

其中形参和返回类型 都是const string的引用,不管是 调用函数 还是 返回结果 都不会真正拷贝string对象

4、不要返回 局部对象的引用 或 指针:函数终止意味着 局部变量的引用 将指向不再有效的内存区域

// 严重错误:这个函数试图返回 局部对象的引用
const string &manip()
{
	string ret;
	// 以某种方式改变一下ret
	if (!ret.empty())
		return ret; // 错误:返回 局部对象的引用
	else
		return "Empty"; // 错误:"empty"是一个局部临时量
}

上面的两条return语句 都将返回未定义的值,所以 两条return语句 都指向了不再可用的内存空间
要想确保返回值安全,引用所引的是 在函数之前已经存在的某个变量

同样,返回局部对象的指针 也是错误的。一旦函数完成,局部对象被释放,指针将指向一个 不存在的对象

5、返回类类型的函数 和 调用运算符:调用运算符 也有 优先级 和 结合律。调用运算符的优先级 与 点运算符和箭头运算符 相同,也符合 左结合律。如果 函数返回指针、引用 或 类的对象,就能使用 函数调用的结果 访问 结果对象的成员
可以通过 如下形式得到较短string对象的长度:

// 调用string对象的size成员,该 string对象是由 shorterString函数返回的
auto sz = shorterString(s1, s2).size();

满足左结合律,所以 shorterString的结果 是点运算符的左侧运算对象,点运算符 可以得到该string对象的size成员,size又是 第二个调用运算符的左侧运算对象

6、引用返回左值:函数的返回类型 决定函数调用是否是左值
调用一个返回引用的函数 得到左值,其他返回类型 得到右值。可以像 使用其他左值那样 来使用返回引用的函数调用,特别的,能为返回类型是 非常量引用的函数的结果 赋值:

char &get_val(string &str, string::size_type ix)
{
	return str[ix];
}

int main()
{
	string s("a value");
	get_val(s, 0) = 'A'; // 将s[0]的值改为A
	return 0;
} 

如果 返回类型是 常量引用,就 不能给调用的结果赋值

7、列表初始化 返回值:C++11,函数可以 返回花括号包围的值的列表。此处的列表也用来 对表示函数返回的临时量进行初始化。如果 列表为空,临时量执行 值初始化(全0 / 全空);否则,返回的值 由函数的返回类型 决定

vector<string> process()
{
	// ...
	if (expected.empty())
		return {}; // 返回一个空vector对象
	else if (expected == actual)
		return {"functionX", "okay"}; // 返回列表初始化的vector对象
	else 
		return {"functionX", expected, actual};

函数返回的是 内置类型,则 花括号包围的列表最多包含一个值,且 该值所占的空间 不应该大于 目标类型的空间。如果 函数返回的是 类类型,由 类自身定义初始值 如何使用

8、主函数main的返回值:
允许main函数 没有return语句 直接结束,编译器 将隐式地插入一条返回0的 return语句
main函数的返回值 可以看作是 状态指示器。返回0 表示执行成功,返回其他值表示 执行失败,其中 非0值的具体含义 依机器而定(与bool相反,bool 0为false,其他值为true)

为了使返回值 与机器无关,cstdlib头文件定义了两个预处理变量,用这两个变量分别表示成功和失败

int main()
{
	if (some_failure)
		return EXIT_FAILURE; // 定义在cstdlib头文件中
	else
		return EXIT_SUCCESS; // 定义在cstdlib头文件中
}

9、递归:一个函数调用了它自身。在递归函数中,一定有 某条路径 是不包含递归调用的

10、什么情况下返回的引用无效?什么情况下返回常量的引用无效?
返回的引用是局部对象的引用,返回的常量引用是局部常量对象的引用时

11、编写一个递归函数,输出vector对象的内容

#include <iostream>
#include <vector>

void read_vi(std::vector<int>::const_iterator iterator_begin, std::vector<int>::const_iterator iterator_end)
{
	if(iterator_begin != iterator_end)
	{
		std::cout << *iterator_begin << " ";
		return read_vi(++iterator_begin, iterator_end);
	}
	else
	{
		std::cout << std::endl;
		return;
	}
}

int main()
{
	std::vector<int> v{1,2,3,4,5};
	read_vi(v.begin(), v.end());
	return 0;
}

附:const_iterator 与 iterator

总结自 C\C++中iterator与const_iterator及const iterator区别

类似于指针
const vector<int>::iterator中,const是修饰的迭代器,也就是是个常迭代器,递增递减操作等都不允许(指iterator本身是const,而非iterator指向的对象)
虽然类似指针,但指针是内置类型,所以编译器可以通过const的位置来判断是常指针还是指向常量的指针,而迭代器只是一个对象,所以编译器不能分辨

所以用const_iterator来取代指向常量的指针,使用它,你通过这个迭代器对迭代器所指向的内容进行改写是非法的,iterator可以改元素值,但const_iterator不可改

vector< int> ivec;
vector< int>::const_iterator citer1 = ivec.begin();
const vector< int>::iterator citer2 = ivec.begin();
*citer1 =  1;  // error
*citer2 =  1;  // right
++citer1;  // right
++citer2;  // error

如果传递过来一个const类型的容器,那么只能用const_iterator来遍历
const_iterator 对象可以用于const vector 或非 const vector,它自身的值可以改(可以指向其他元素),但不能改写其指向的元素值

12、在调用factorial函数时,为什么我们传入的值是 val-1 而非 val–?

#include <iostream>

using std::cout; using std::endl;

int factorial(int val)
{
	if (val > 1)
		return factorial(val-1) * val;
	return 1;
}

int main()
{
	cout << factorial(4) << endl;
	return 0;
}

val–会返回未修改的val内容(factorial(val–)永远不变),使程序陷入无限循环

3.3 返回数组指针

1、数组 不能被拷贝,所以 函数不能返回数组。函数可以返回数组的指针 或 引用。其中最直接的方法是 使用类型别名

typedef int arrT[10]; // arrT是一个类型别名,它表示的类型 是含有10个整数的数组
using arrT = int[10]; // arrT的等价声明
arrT* func(int i); // func返回一个指向含有10个整数的数组的指针

无法返回数组,所以 将返回类型定义成 数组的指针

2、声明一个 返回数组指针的函数:要想在声明func时 不使用类型别名,必须要 牢记被定义的名字后面数组的维度
函数的形参列表 也跟在函数名字后面 且形参列表应该先于 数组的维度

Type (*func(int i)) [10];

3、使用 尾置返回类型:简化上述func声明的方法,就是 使用尾置返回类型。任何函数的定义 都能使用尾置返回,但是 这种形式对于返回类型比较复杂的函数(数组的指针 或 数组的引用) 最有效

尾置返回类型 跟在形参列表后面 并以一个->符号开头。为了表示 函数真正的返回类型 跟在形参列表之后,在本该出现返回类型的地方 放置一个auto

// func接受一个int类型的实参,返回一个指针,该指针 指向含有10个整数的数组
auto func(int i) -> int(*)[10];

4、使用decltype:知道函数返回的指针 将指向哪个数组,就可以使用decltype关键字声明返回类型
如:函数返回一个指针,该指针 根据参数i的不同 指向两个数组中的某一个

int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
// 返回一个指针,该指针 指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
	return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}

arrPtr使用关键字 decltype表示 它的返回类型 是个指针,该指针所指的对象 与odd的类型一致
arrPtr 返回一个指向 含有5个整数的数组的指针

decltype并不负责 把数组类型 转换为 对应的指针,所以 decltype的结果是个数组,要想表示arrPtr返回指针 还必须 在函数声明时 加一个*符号

5、编写一个函数声明,使其 返回数组的引用并且该数组包含10个string对象

// 不用使用尾置返回类型、decltype或者类型别名
std::string (&fun(std::string (&arrs)[10]))[10];

//使用类型别名
using ARRS = std::string[10];
ARRS &fun(ARRS &arrs);

// 使用尾置返回类型
auto fun(std::string (&arrs)[10]) -> std::string (&)[10];

// 使用decltype关键字
std::string arrs1[10];
decltype(arrs1) &fun(decltype(arrs1) &arrs);

4、函数重载

1、同一作用域内的 几个函数名字相同 但形参列表不同,main函数 不能重载

2、不允许 两个函数除了返回类型以外 其他所有要素都相同。假设有两个函数,他们的形参列表一样 但返回值类型不同,则 第二个函数的声明是错误的

Record lookup(const Account&);
bool lookup(const Account&); // 错误:与上一个函数 相比只有返回类型不同

3、判断 两个形参的类型 是否相异:两个形参列表 看起来不同,但 实际上是相同的

// 每对声明的是 同一个函数
Record lookup(const Account &acct);
Record lookup(const Account&); // 省略了形参的名字

typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno和Phone的类型相同

形参的名字 仅仅起到 帮助记忆的作用,有没有它 并不影响形参列表的内容

4、重载和const形参
顶层const不影响传入函数的对象。一个拥有顶层const的形参 无法和 另一个没有顶层const的形参 区分开来

Record lookup(Phone);
Record lookup(const Phone); // 重复声明

Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明

如果 形参是某种类型的指针 或 引用,则 通过区分其指向的是 常量对象 还是 非常量对象 可以实现函数重载,此时的const是底层的:

// 对于接受引用 或 指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了4个独立的重载函数
Record lookup(Account&); // 函数作用于Account的引用
Record lookup(const Account&); // 新函数,作用于 常量引用

Record lookup(Account*); // 新函数,作用于 指向Account的指针
Record lookup(const Account*); // 新函数,作用于 指向常量的指针

编译器可以通过 实参是否是 常量 来推断应该调用哪个函数。只能把const对象(或 指向const的指针)传递给const形参
因为 非常量可以转换成const,所以 上面的4个函数都能作用于 非常量对象 或者 指向非常量对象的指针。当我们传递 一个非常量对象 或者 指向非常量对象的指针时,编译器 会优先选用 非常量版本的函数

5、const_cast 和重载:const_cast(4.11.3) 在重载类型中最有用,以3.2节的shorterString函数为例

// 挑出两个string对象中 较短的那个,返回其引用
const string &shorterString(const string &s1, const string &s2)
{
	return s1.size() <= s2.size() ? s1 : s2;
}

这个函数的参数和返回类型 都是const string的引用。可以对 两个非常量的string实参 调用这个函数,但 返回的结果仍然是 const string的引用

因此需要新的 shorterString函数,当他的实参 不是常量时,得到的结果是一个 普通的引用,使用 const_cast可以做到这一点:

string &shorterString(string &s1, string &s2)
{
	auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
	return const_cast<string&>(r);
}

它的实参 强制转换成对 const的引用,然后调用了shortString函数的const版本。const版本返回对 const string的引用,这个引用 事实上绑定在了 某个初始的非常量实参上。我们可以再将其 转换回一个 普通的string&,这显然是安全的

6、调用重载的函数:函数的匹配 也叫 重载确定,把函数调用 和 一组重载函数中的 某一个关联起来

调用重载函数时 有三种可能的结果:
1)编译器 找到一个与实参最佳匹配 的函数,并生成 调用该函数的代码
2)找不到任何一个函数 与调用的实参匹配,此时 编译器发出无匹配的错误信息
3)有多于一个函数可以匹配,但是 每一个都不是明显的最佳选择。此时 也将发生错误,称为 二义性调用

以下声明是否合法?

int calc(int, int);
int calc(const int, const int); 
// 合法,可以重复声明,编译器通过实参是否是常量 来推测应该用哪个函数

int get();
double get();
// 非法,仅返回值不同

int *reset(int *);
double *reset(double *); // 合法,重载

4.1 重载 与 作用域

1、将函数声明 置于局部作用域内 不是一个明智的选择。如果 在内层作用域内声明名字,将隐藏外层作用域中 声明的同名实体。在不同的作用域中 无法重载函数名:

string read();
void print(const string &);
void print(double); // 重载print函数
void fooBar(int ival)
{
	bool read = false; // 新的作用域,隐藏外层作用域中 声明的同名实体
	string s = read(); // 错误:read是一个布尔值,而非函数,被隐藏了
	
	// 不好的习惯:通常来说,在局部作用域中 声明函数 不是一个好的选择
	void print(int); // 新作用域:隐藏之前的print
	print("Value:"); // 错误:print(const string &)被隐藏掉了
	
	print(ival); // 正确:当前print(int)可见
	print(3.14); // 正确:调用print(int);print(double)被隐藏了
}

当编译器处理调用read的请求时,找到的是 定义在局部作用域中的read。这个名字是个 布尔变量,而我们显然 无法调用一个布尔值,因此 该语句非法

调用print函数的过程相似。在fooBar内声明的print(int) 隐藏了之前两个print函数,因此 只有一个print函数是可用的;该函数以 int值作为参数
调用print函数时,编译器 首先寻找对该函数的声明,找到的是 接受int值的那个局部声明。一旦 在当前的作用域中 找到了所需的名字,编译器 就会忽略掉 外层作用域中的同名实体。剩下的工作 就是检查函数调用 是否有效了

假设把print(int)和其他print函数声明 放在同一个作用域中,则 它将成为 另一种重载形式。因为 编译器能看见所有三个函数,上述调用的处理结果 将完全不同

void print(const string &);
void print(double); // print函数的另一种 重载形式
void print(int); // print函数的另一种重载形式
void fooBar2(int ival)
{
	print("Value:"); // 调用print(const string &)
	print(ival); // 调用print(int)
	print(3.14); // 调用print(double)
}

2、C++中 名字查找 发生在 类型检查之前

5、特殊用途语言特性

三种函数相关的语言特性:默认实参、内联函数 和 constexpr函数

5.1 默认实参

1、在函数的很多次调用中 它们都被赋予一个 相同的值,此时 这个反复出现的值 称为函数的默认实参。调用 含有默认实参的函数时,可以包含该实参,也可以省略该实参

为了使窗口函数 既能接纳默认值,也能接受 用户指定的值

typedef string::size_type sz; 
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' '); 

为每一个形参都提供了 默认实参,默认形参 作为形参的初始值 出现在形参列表中。我们可以为 一个或多个形参定义 默认值,一旦 某个形参 被赋予了默认值,其后面的所有形参 都必须有默认值

如:char *init(int ht = 24, int wd, char backgrnd) 错误,因为 默认形参右侧的所有参数 都必须有默认值
char *init(int ht, int wd = 80, char backgrnd = ' ') 就正确了

string window;
window = screen(); // 等价于 screen(24, 80, ' ')
window = screen(66); // 等价于 screen(66, 80, ' ')
window = screen(66, 256. '#'); // 等价于screen(66, 256, '#')

函数调用时 实参按其位置解析,默认实参 负责填补函数调用 缺少的尾部实参(靠 右侧位置),要想覆盖backgrnd的默认值,必须为 ht和wid提供实参

window = screen(, , '?'); // 错误:只能省略尾部的实参
window = screen('?'); // 调用 screen('?', 80, ' ')

第二个调用是 合法调用,‘?’ 是个char,而函数最左侧形参的类型 string::size_type是一种无符号 整数类型。当 该调用发生时,char类型的实参 隐式的转换为 string::size_type,然后作为height的值传递给 函数

当设计 含有默认实参的函数时,一项任务 就是 合理设置形参的顺序,尽量 让不怎么使用默认值的形参 出现在前面,而 让那些经常使用 默认值的形参 出现在后面

2、默认实参声明:对于 函数的声明来说,通常 将其放在头文件中,一个函数只声明一次,多次声明同一个函数 时合法的。在给定作用域中 一个形参 只能被赋予一次 默认实参。函数的后续声明 只能为之前那些 没有默认值的形参 添加默认实参,而且 该形参右侧的所有形参 必须有默认值

// 表示高度 和 宽度的形参没有默认值
string screen(sz, sz, char = ' ');
// 不能修改一个已经存在的默认值
string screen(sz, sz, char = '*'); // 错误:重复声明
// 可以按照如下形式添加默认实参
string screen(sz = 24, sz = 80, char); // 正确:添加默认实参

3、在函数声明中 指定默认实参,并将该声明 放在合适的头文件中

4、默认实参初始值:局部变量 不能作为默认实参。只要 表达式的类型能转换成 形参所需的类型,该表达式 就能作为 默认实参:

// wd、def 和 ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // 调用 screen(ht(), 80, ' ')

用作默认实参的名字 在函数声明所在的作用域内解析,其他作用域实参改了 不影响函数调用的计算,而 这些名字的求值过程 发生在函数调用时

void f2()
{
	def = '*'; // 改变默认实参的值
	sz wd = 100; // 隐藏了外层定义的wd,但是没有改变 默认值
	window = screen(); // 调用screen(ht(), 80, '*')
}

函数还声明了 一个全局变量 用于隐藏外层的wd,但是 该局部变量与传递给screen的默认实参 没有任何关系

5、给make_plural函数的第二个形参赋予默认实参’s’, 利用新版本的函数输出单词success和failure的单数和复数形式

#include <iostream>
#include <string>

using std::string;

string make_plural(size_t ctr, const string &word, const string &ending = "s")
{
	return (ctr > 1) ? word + ending : word;
}

int main()
{
	std::cout << make_plural(2, "success", "es") << std::endl;
	std::cout << make_plural(2, "failure") << std::endl;
}

5.2 内联函数 和 constexpr函数

1、把这种规模较小的操作定义成 函数 的好处:
1)阅读理解函数的调用 比 读懂等价的条件容易
2)使用函数 可以确保行为的统一
3)如果需要 修改计算过程,修改函数 比先找到 等价表达式所有出现的地方 再逐一修改更容易
4)函数 可以被其他应用 重复利用

调用函数 一般比求 等价表达式的值要慢一点。一次函数调用 其实包含一系列工作:调用前要先保存 寄存器,并在 返回时 恢复;可能需要拷贝实参;程序转向一个新的位置 继续执行

2、内联函数 可避免函数调用的开销:在 每个调用点上内联地 展开,把shorterString 函数定义为 内联函数

cout << shorterString(s1, s2) << endl;

编译过程中 展开成类似于下面的形式

cout << (s1.size() < s2.size() ? s1 : s2) << endl;

从而消除了shortString函数的运行时开销。在shortString函数的返回类型前面 加上关键词inline,这样 就可以将它声明为 内联函数

// 内联版本:寻找两个string对象中 较短的那个
inline const string &shorterString(const string &s1, const string &s2)
{
	return s1.size() <= s2.size() ? s1 : s2;
}

只是向 编译器发出一个请求,编译器 可以选择忽略这个请求
内联机制 用于优化 规模较小、流程直接、频繁调用的函数

3、constexpr函数:能用于常量表达式的函数,定义要求:
1)函数的返回类型 及 所有形参的类型 都得是 字面值类型
2)函数体中 必须有且只有一条return语句

constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // 正确:foo是一个常量表达式

执行初始化任务时,编译器把对constexpr函数的调用 替换成其 结果值,为了能在编译过程中 随时展开,constexpr 函数被隐式地指定为 内联函数

constexpr函数体内也可以包含其他语句,只要 这些语句在运行时 不执行任何操作就行。constexpr函数中 可以有空语句、类型别名 以及 using声明,常量表达式

允许constexpr函数的返回值 并非一个常量

// 如果 arg是常量表达式,则 scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }

当scale的实参是 常量表达式,它的返回值也是常量表达式;反之 不然:

int arr[scale(2)]; // 正确,scale(2)是常量表达式
int i = 2; // i不是常量表达式
int a2[scale(i)]; // 错误:scale(i)不是常量表达式

给 scale函数 传入一个 形如字面值2的常量表达式时,它的返回类型 也是常量表达式

4、constexpr函数 不一定返回 常量表达式
用一个 非常量表达式 调用 scale函数,比如 int类型的对象i,则返回值 是一个非常量表达式。当把scale函数 用在需要常量表达式的上下文中时,由编译器检查函数的结果 是否符合要求。如果 恰好不是常量表达式,编译器将发出 错误信息

5、把 内联函数和constexpr函数 放在头文件内:和其他函数不一样,内联函数 和 constexpr函数 可以在程序中多次定义。毕竟,编译器要想展开函数 仅有函数声明是不够的,还需要函数的定义。对于 某个给定的内联函数 或者 constexpr函数来说,它的多个定义 必须完全一致,因此 内联函数和constexpr函数 放在头文件内

把下面的哪个声明和定义放在头文件中?

(a) inline bool eq(const BigInt&, const BigInt&) {...}
(b) void putValues(int *arr, int size);

(a)放在头文件中,内联函数在程序中可以多次定义,它的多个定义必须完全一致,所以放在头文件中比较好;
(b)放在头文件中,声明放在头文件中

回顾在前面的练习中编写的那些函数,它们应该是内联函数吗?
6.38和6.42应该是内联函数;6.4不应该是,规模较小,调用不频繁

函数短小的,没有循环 以及 递归的 应该被定义为内联函数

5、能把isShorter函数定义成constexpr函数吗
因为 isShorter 函数中 传入的常数不是 字面值类型,str1.size() < str2.size() 返回的也不是 字面值类型

5.3 调试帮助

1、C++有时 会用到一种类似于头文件保护的技术,以便 有选择地执行调试代码。基本思想是,程序可以包含一些 用于调试的代码,但是这些代码 只在开发程序时使用。当 应用程序编写完成 准备发布时,要先 屏蔽掉调试代码。这种方法 用到两种预处理功能:assert 和 NDEBUG

2、assert预处理宏:是一个预处理变量,它的行为类似于内联函数。assert宏 使用一个表达式作为它的条件:assert (expr); 首先 对expr求值,如果表达式为假(0),assert输出信息 并终止程序的执行。如果 表达式为真(非0),assert什么也不做

3、assert宏定义在 cassert头文件中。预处理名字 由预处理器 而非编译器管理,可以直接 使用预处理名字 而无需提供using声明。应该使用assert而不是 std::assert

4、和预处理变量一样,宏名字在程序内 必须唯一,含有 cassert头文件的程序 不能再定义名为 assert的变量、函数 或者 其他实体。在实际编程中,即使没有包含cassert头文件,也最好不要为了 其他目的使用assert。很多 头文件包含了cassert,意味着 即使没有直接包含 cassert,也很可能 通过其他途径 包含在程序中

5、assert宏 常用于检查“不能发生”的条件
如:一个对输入文本 进行操作的程序 要求所有给定单词的长度 都大于某个阈值,程序可以包含 assert(word.size() > threshold);

assert使用例子:

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

int main()
{
	string s;
	while (cin >> s && s != "end") { cout << 1 << endl; } //空函数体
	assert(cin);
	return 0;
}

输入end 正常退出,ctrl+z 输出如下
ctrl+z输出结果
不合理,这里虽然没有语法错误,但是它的使用是不合理的。 assert 宏通常用于检查“不能发生”的条件。这里改成 assert(!cin || s == "end") 更加合理些

6、NDEBUG预处理变量:assert的行为 依赖于一个名为NDEBUG的预处理变量的 状态。如果定义了NDEBUG,则assert什么也不做。默认状态下 没有定义NDEBUG,此时 assert将执行运行时检查

使用一个#define语句定义NDEBUG,从而关闭 调试状态。可以在main.c文件一开始 写#define NDEBUG

7、定义NDEBUG能避免检查 各种条件所需的运行时开销,当然 此时不会执行任何运行时检查。因此,assert应该仅用于 验证那些确实不可能发生的事情。可以把assert当成调试程序的一种辅助手段,但是 不能用它代替真正的运行时逻辑检查,也不能代替 程序本身应该包含的错误检查

8、除了定义assert外,也可以单独使用NDEBUG编写 自己的条件调试代码。如果 NDEBUG未定义,将执行#ifdef 和 #endif之间的代码;如果 定义了NDEBUG,这些代码会被忽略

void print(const int ia[], size_t size)
{
#ifndef NDEBUG
	// __func__是编译器定义的一个局部变量,用于 存放函数的名字,是 const_char的一个静态数组
	cerr << __func__ << ": array size is " << size << endl;
#endif

除了C++编译器定义的 __func__ 之外,预处理器 还定义了 另外4个对于程序调试 很有用的名字

__FILE__ 存放文件名的字符串字面值
__LINE__ 存放当前行号的整形字面值
__TIME__ 存放文件编译时间的字符串字面值
__DATE__ 存放文件编译日期的字符串字面值

这些常量在错误信息中提供更多信息
使用递归输出vector内容的程序,使其有条件地输出与执行过程有关的信息。例如,每次调用时输出vector对象的大小。分别在打开和关闭调试器的情况下编译并执行这个程序

#include <iostream>
#include <vector>
#include <cassert>

//#define NDEBUG

void read_vi(std::vector<int>::const_iterator iterator_begin, std::vector<int>::const_iterator iterator_end)
{
#ifndef NDEBUG
	std::cerr << iterator_end - iterator_begin <<" " << __func__ << " " << __FILE__ << " "
		<< __LINE__ << " " << __TIME__ << " " << __DATE__ << std::endl;
#endif

	if (iterator_begin != iterator_end)
	{
		std::cout << *iterator_begin << " ";
		return read_vi(++iterator_begin, iterator_end);
	}
	else
	{
		std::cout << std::endl;
		return;
	}
}

int main()
{
	std::vector<int> v{ 1,2,3 };
	read_vi(v.begin(), v.end());
	return 0;
}

执行结果:
执行结果

6、函数匹配

1、当几个重载函数的形参数量相等 以及 某些形参的类型 可以由其他类型转换而来时

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // 调用void f(double, double)

1)确定候选函数:
函数匹配的第一步是 选定本次调用 对应的重载函数集。集合中的函数称为 候选函数
候选函数有两个特征:(1)与被调用的函数同名 (2)其声明在调用点可见

在这个例子中有四个名为f的候选函数

2)确定可行函数:
考察本次调用 提供的实参,然后从 候选函数中选出 能被这组实参调用的函数,这些新选出的函数 称为可行函数
可行函数有两个特征:(1)其形参数量 与 本次调用 提供的实参数量相等 (2)每个实参的类型 与对应的形参类型相同,或者 能转换成 形参的类型

本例中f(int)、f(double, double) 是可行的

函数含有默认实参,则我们 在调用该函数时 传入的实参数量 可能 少于它实际使用的实参数量

3)寻找最佳匹配:
第三步 是从可行函数中 选择与本次调用 最匹配函数,逐一检查函数调用的实参,寻找 形参类型 和 实参类型 最匹配的那个可行函数。实参类型 与 形参越接近,他们匹配的越好

在例子中,调用只提供了一个(显式的)实参,它的类型是double。如果 调用f(int),实参 将不得不从 double 转换成int。另一个可行函数 f(double, double) 则与实参精确匹配

2、含有多个形参的函数匹配
考虑函数调用:f(42, 2.56);

选择可行函数的方法 和只有一个实参时一样,编译器 选择形参数量满足要求 且 实参类型和形参类型 能够匹配的函数
例子中可行函数包括f(int, int)和f(double, double),接下来编译器 依次检查每个实参 以确定哪个函数是最佳匹配。如果 有且只有一个函数 满足下列条件,则匹配成功:
1)该函数 每个实参的匹配 都不劣于其他可行参数需要的匹配
2)至少有一个实参 的匹配优于 其他可行函数提供的匹配

编译器最终将因为这个调用具有二义性 而拒绝其请求:因为 每个可行函数 各自在一个实参上 实现了更好的匹配,从整体上 无法判断孰优孰劣
在设计良好的系统中,不应该 对实参进行 强制类型转换。调用重载函数时 应尽量避免强制类型转换。如果在实际应用中 确实需要强制类型转换,则说明 我们设计的形参集合 不合理

6.1 实参类型转换

1、为了确定最佳匹配,编译器将 实参类型 到 形参类型的转换划分成几个等级(有高低之分)
1)精确匹配
(1)实参形参类型相同
(2)实参 从数组类型 或 函数类型 转换成对应的指针的类型
(3)向实参添加顶层const 或者从实参中 删除顶层const

2)通过const转换 实现的匹配(4.11.2)
3)通过类型提升 实现的匹配(4.11.1)
4)通过算数类型转换(4.11.1) 或指针转换(4.11.2)实现的匹配
5)通过 类类型转换实现的匹配

2、需要 类型提升和算术类型转换的匹配:假设 有两个函数,一个接受int、另一个接受short,则只有当调用提供的是short类型的值时 才会选择short版本的函数
即使 实参是一个很小的整数值,也会 直接将它提升成int类型;此时 使用short版本 反而会导致类型转换

void ff(int);
void ff(short);
ff('a'); // char提升成int;调用f(int)

所有算术类型转换的级别 都一样。例如,从int向unsigned int的转换 并不比 从int向double的转换 级别高

void manip(long);
void manip(float); 
manip(3.14); // 错误:二义性调用

字面值3.14的类型是 double,它既能转成long 也能转成 float,该调用具有 二义性

3、函数匹配 和 const实参:如果 重载函数的区别在于 他们的引用类型的形参 是否引用了const,或者 指针类型的形参是否指向const,当调用发生时 编译器通过实参 是否是常量来决定选择哪个函数

Record lookup(Account&); // 函数的参数是Account的引用
Record lookup(const Account&); // 函数的参数是一个常量引用
const Account a;
Account b;

lookup(a); // 调用lookup(const Account&)
lookup(b); // 调用lookup(Account&)

在第一个调用中,传入的是const对象a。因为 不能把普通引用 绑定到 const对象上。唯一可行的是以常量引用作为形参的那个函数

对于第二个调用,两个函数都是可行的,用 非常量对象初始化 常量引用 需要类型转换,接受 非常量形参的版本 则与b精确匹配

4、指针类型的形参 也类似。如果 两个函数的唯一区别 是它的指针形参 指向 常量或非常量,则 编译器能通过实参 是否是常量决定选用哪个函数;如果 实参是指向常量的指针,调用形参是 const*的函数;如果实参是指向 非常量的指针,调用形参是普通指针的函数

5、拷贝是语义,赋值是语法;初始化不是赋值,初始化的含义是在创建对象时赋予一个初值,而赋值是将对象的当前值擦除掉,以一个新值代替

6、指出下列调用中每个类型转换的等级

manip('a', 'z');
manip(55.4, dobj);

3等级,通过类型提升实现的匹配;
4等级,通过算数类型转换

int calc(char*, char*);
int calc(const char*, const char*);

int calc(char*, char*);
int calc(char* const, char* const);

合法,实参可以为const char*;
合法,顶层const,声明重复(可以重复声明,不可重复定义)

7、函数指针

1、函数指针指向的是 函数而非对象,和其他指针一样,函数指针指向 某种特定类型。函数的类型 由它的返回类型和形参类型 共同决定,与 函数名无关

// 比较两个string对象的长度
bool lengthCompare(const string &, const string &);

2、声明函数指针:该函数的类型是 bool(const string &, const string &)。声明一个可以指向该函数的指针,只需要 用指针替换函数名即可

// 声明指针pf指向一个函数,该函数的参数是两个const string的引用,返回值是 bool类型
bool (*pf) (const string &, const string &); // 未初始化

pf前面有个*,因此pf是指针。*pf两端的括号必不可少。如果 不写这对括号,则 pf是一个返回值为bool指针的函数

3、使用函数指针:当把 函数名 作为一个值使用时,该函数自动转换成指针

pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋值语句:取地址符时可选的

还能直接 使用指向函数的指针调用该函数,无需 提前解引用指针

bool b1 = pf("hello", "goodbye"); // 调用 lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用

在指向不同函数类型的指针间 不存在 转换规则。可以为函数指针 赋一个nullptr 或者 值为0的整型常量表达式,表示 该指针没有指向任何一个函数

string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);

pf = 0; // 正确:pf不指向任何函数
pf = sumLength; // 错误,sumlength返回的是 string::size_type* 不是 bool*
pf = cstringCompare; // 错误,形参不匹配
pf = lengthCompare; // 正确,函数和指针的类型 精确匹配

4、重载函数的指针:如果定义了指向重载函数的指针

void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff; // 定义pf1指向ff(unsigned)

编译器通过指针类型 决定选用哪个函数,指针类型必须与 重载函数中的某一个 精确匹配(包括 返回类型 和 形参列表)

void (*pf2)(int) = ff; // 错误,没有任何一个ff与 该形参列表匹配
double (*pf3)(int*) = ff; // 错误,ff和pf3的返回类型不匹配

5、函数指针形参:和数组类似,虽然不能定义 函数类型的形参,但是 形参可以是 指向函数的指针,此时形参看起来是 函数类型,实际上 却是当成 指针使用

// 第三个形参是 函数类型,它会自动地转换成 指向函数的指针
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// 等价的声明
void useBigger(const string &s1, const string &s2, bool (*pf)(const string&, const string&));

还可以直接把函数作为 实参使用,此时 它会自动转换成 指针

// 自动将函数lengthCompare 转换成 指向该函数的指针
useBigger(s1, s2, lengthCompare);

6、使用typedef(类型别名)和 decltype 能简化 使用函数指针的代码

// 取的别名:Func和Func2 都是函数类型
typedef bool Func(const string&, const string&); // 注意别名Func的位置
typedef decltype(lengthCompare) Func2; // 等价的类型

// 取的别名:FuncP 和 FuncP2 都是指向函数的指针
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;  // 等价的类型

类型别名 Func和Func2是函数类型,而 FuncP 和 FuncP2 是指针类型。decltype返回函数类型 不会将函数类型 自动转换为 指针类型。因为decltype的结果 是函数类型,所以 只有在结果前面 加上*才能得到 指针

7、返回指向函数的指针:和数组类似,虽然 不能返回一个函数,但是能返回 指向函数类型的指针。我们必须把 返回类型 写成 指针形式,编译器 不会自动将函数返回类型 当成对应的指针类型 处理。要想声明一个 返回函数指针的函数,最简单的办法 是使用类型别名

using F = int(int*, int); // F是函数类型,不是指针
using PF = int(*)(int*, int*); // PF是指针类型

和函数类型的形参 不一样,返回类型不会自动转换为 指针。必须 显式地将返回类型指定为 指针

PF f1(int); // 正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int); // 错误:F是函数类型,f1不能返回一个函数
F *f1(int); // 正确:显式地指定 返回类型是指向函数的指针

也能用 下面的形式 直接声明f1(等价的)

int (*f1(int))(int*, int);

看到f1有形参列表((int)),所以f1是个函数;f1前面有*,所以f1返回的是个 指针;指针的类型本身 包含形参列表((int*, int)),因此 该指针 指向函数,返回类型是int

还可以使用 尾置返回类型的方式 声明 一个等价的 返回函数指针的 函数

auto f1(int) -> int (*)(int*, int);

附:定义类型别名 typedef 与 using

摘自 C++ 类型别名typedef、using的区别
类型别名 和类型的名字等价,只要是类型的名字能出现的地方,就可以使用 类型别名。C++中 有两种方法 可以定义类型别名,分别是 使用关键字typedef、关键字using

typedef 和 using 都可以用于定义类型别名,using (using 别名 = 原类型;) 是 C++11 及以后版本中引入的新特性,它可以用于模板别名和别名声明
而 typedef (typedef 原类型 别名;) 是 C++ 中原有的定义类型别名的方式,它无法用于模板别名和别名声明
using支持模板别名、别名声明以及更好的依赖名称查找规则。在现代 C++ 中,建议优先使用 using 而非 typedef

8、将 auto和decltype用于函数指针 类型:假如 假定有两个函数,它们的返回类型都是 string::size_type,并且 各有类型相同的 两个const string&类型的形参
此时 可以编写第三个函数,可以 接受一个string类型的参数,返回一个指针,该指针 指向前两个函数中的一个

string::size_type sumLength(const string&, const string&);
string::size_type largeLength(const string&, const string&);

// 根据形参的取值(sumLength / largeLength),getFcn函数返回 指向sumLength 或者 largerLength的指针
decltype(sumLength) *getFcn(const string&);

9、编写4个函数,分别对两个int值执行加、减、乘、除运算,调用vector对象中的每个元素并输出结果

#include <iostream>
#include <vector>
using namespace std;
typedef int (*P)(int, int);

int func1(int a, int b) {
    return a + b;
}

int func2(int a, int b) {
    return a - b;
}

int func3(int a, int b) {
    return a * b;
}

int func4(int a, int b) {
    return a / b;
}

void compute(int i, int j, P p) {
    cout << p(i, j) << endl;
}

int main() {
    int i = 5, j = 10;

    // 等价于 decltype(func1)* p1 = func1, * p2 = func2, * p3 = func3, * p4 = func4;
    // 函数名返回指针
    P p1 = func1, p2 = func2, p3 = func3, p4 = func4;

    // 等价于 vector<decltype(func1)*> vF = { p1, p2, p3, p4 };
    vector<P> vF = { p1, p2, p3, p4 };

    for (auto p : vF) {
        // 遍历 vector 中的每个元素,依次调用四则运算函数
        compute(i, j, p);
    }
    return 0;
}

输出
代码输出结果

术语表

1、实参:函数调用时 提供的值,用于初始化 函数的形参

2、自动对象:仅存在于 函数执行过程中的对象。当程序的控制流 经过此类对象的定义语句时,创建 该对象;当到达了定义所在块的末尾时,销毁 该对象

3、函数原型:函数的声明,包含 函数名字、返回类型 和 形参类型。要想调用某函数,在调用点之前 必须声明 该函数的原型

4、对象文件:编译器根据 给定的源文件生成的 保存对象代码的文件。一个或多个 对象文件经过 链接 生成可执行文件

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值