1 传递数组给函数
1.1 传递数组形参的三种写法1.2 传递数组尺寸给函数
1.2.1 使用标记指定数组长度1.2.2 使用标准库规范1.2.3 显式传递一个表示数组大小的形参 1.3 数组形参和const1.4 数组引用形参 2 返回类型与return语句
2.1 有、无返回值的函数及其return语句2.2 函数把值返回给调用处的细节原理
2.2.1 函数返回局部变量给外部2.2.2 函数返回引用给外部 2.3 返回类类型的函数可以连续调用2.4 引用返回左值2.5 返回数组指针
2.5.1 用一般方式声明一个返回数组指针的函数2.5.2 使用类型别名来简化2.5.3 使用尾置返回类型(C++11新特性)来简化 3 重载
3.1 重载和const形参3.2 重载与作用域
1 传递数组给函数数组的两个特殊性质决定了数组在函数作用中的特殊性:
1.1 传递数组形参的三种写法性质1:不允许拷贝数组。因此不能以值传递的方式将数组传给函数。性质2:使用数组时(通常)会将其转换成指针(指向首元素地址)。因此向函数传递数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组给函数,但我们可以把形参写成形式上的数组形式。此时如果我们直接传给函数的是一个数组(而不是显式的指向数组首地址的指针),根据使用数组时的自动转换原则,编译器也会让实参(传进去的数组名)自动地转成指向数组首元素的指针。
范例:
//虽然形式不同,但以下三个函数等价,因为每个函数的形参类型(忽略形参名)其实都是const int *类型
void print(const int *);
void print(const int[]);
void print(const int[10]) //以上三个写法,int和*或[]之间有没有空格都是正确的
//请特别注意,以上三个函数声明中,显式声明其形参名的写法应该如下
void print(const int *arr);
void print(const int arr[]);
void print(const int arr[10]);
//后两种写法要特别注意不要错误写成以下形式
void print(const int[] arr); //❌
void print(const int[10] arr); //❌
//编译器处理对print函数的调用时,只检查传入的参数是否是const int *类型
int i = 0, j[2] = {0, 1};
print(&i); //正确,&i的类型是int *,int *对象(实参)可以用来初始化const int *对象(形参)
print(j); //正确,使用数组j时,自动将数组名(j)转换成指向数组首元素的指针(int *)类型,也就是说j现在其实是一个指向j[0]的int *对象,当然也可以用int *对象(实参j)来初始化const int *对象(形参arr)
1.2 传递数组尺寸给函数
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道传入数组的确切尺寸,因此,需要把数组尺寸通过额外的信息传递给函数,这主要有三种方法:
1.2.1 使用标记指定数组长度这种方法要求数组本身包含一个结束标记,该方法不需要传参时传递代表尺寸的额外信息。典型范例是C风格字符串,因为C风格字符串存储在字符数组内,且在最后一个字符后还有一个空字符作为结束标记,函数在处理C风格字符串时遇到空字符则停止,范例:
void print(const char *cp) {
if (cp) { //若cp不是一个空指针
while (*cp) { //只要指针cp所指的字符不是空字符
cout << *cp++; //输出当前字符(先cout *cp),并将指针向前移动一个位置(再cp++)
}
}
}
1.2.2 使用标准库规范
这种方法通过向函数传递指向数组首元素和尾后元素的指针来间接传递数组的尺寸并实现对数组的操作,这是标准库技术中的常用规范:
void print(const int *beg, const int *end) { //注意,end是指向数组尾元素的后一个位置的指针
//输出beg到end之间(不包含end)的所有元素
while (beg != end) {
cout << *beg++ <
1.2.3 显式传递一个表示数组大小的形参
最自然的想法,是C程序和较老的C++程序中常用的方法:
//注意,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;
}
}
//调用此函数时需要先求出数组大小并传入,大小可以通过end函数获得的尾后指针减begin函数获得的首指针得到
int j[] = {0, 1};
print(j, end(j)-begin(j));
1.3 数组形参和const
考虑传数组形参是否要用const修饰,请遵守以下原则:
当函数不需要对数组元素执行写操作时,数组的形参应该时指向const对象的指针(底层const)当函数确实要改变数组元素值时,才把形参定义为指向非常量的指针
1.4 数组引用形参
因为C++允许将变量定义为数组的引用,因此,形参也可以是数组的引用。参数传递时,引用形参绑定到对应的实参(也就是传入数组)上。需要特别注意的是用数组的引用作为形参时,符号&及引用名两端的括号(),必不可少,否则会声明为引用的数组。
//&arr两端的括号必不可少,两者的区别是形参代表的是引用的数组还是数组的引用
void func(int &arr[10]); //形参arr代表的是一个存储10个int类型引用的数组
void func(int (&arr)[10]); //arr是存储10个int类型数组的引用
void print(int (&arr)[10]) { //一个正确声明arr是int类型数组的引用的写法,后面的维度[10]是引用所绑定的类型的一部分
for (auto elem : arr) {
cout << elem << endl;
}
}
2 返回类型与return语句
2.1 有、无返回值的函数及其return语句
无返回值(函数声明中,返回值类型为void)的函数,如果其中没有任何return语句也是正确的,编译器会自动在函数结束处补上隐式的return;语句。如果这种void函数内部出现显式的return;,其作用是表示该函数在此处将控制权交还给主调函数。除void函数外,其他函数必须显式地返回与声明中返回值类型相同(或可以转换为该类型)的值。
2.2 函数把值返回给调用处的细节原理
函数把值返回给外部调用处的实现方法细节与从外部实参传递值给函数形参的方法细节完全一样。本质上是用已有值(传参中的实参,返回中的返回值)初始化一个临时量(传参中的形参,返回中的函数调用点)。因此对象的初始化规则在函数返回过程中仍然适用。
2.2.1 函数返回局部变量给外部
本质上是将函数内局部变量的值拷贝给函数调用点,用来初始化(外部)调用点的临时对象。
2.2.2 函数返回引用给外部
引用是对象的别名,因此此时函数对外返回的是对象本身,而不是拷贝一个值给外部副本。因此要特别注意,千万不能返回函数内部局部对象的引用和指针。因为函数在结束调用后局部变量就会被销毁,这时被函数给返回到外部的局部变量对象(引用)将不再绑定有效的内存区域。
综上可知,函数要想对外返回引用(或指针),只能返回那种在函数调用之前已经存在了的对象的引用(或指向该对象的指针)
范例:
//函数对外返回引用的正确用法
const string &shorterStr(const string &s1, const string &s2) { //挑出两个string对象中较短的那个,返回其引用
return s1.size() <= s2.size() ? s1 : s2; //因为s1和s2绑定的对象都是在调用函数前就已经存在的了,因此函数对外返回这种对象的引用不会产生问题
}
//以下写法严重错误,这个函数试图返回局部对象的引用
const string &manip() {
string ret;
if (!ret.empty()) {
return ret; //❌,试图返回局部对象的引用
} else {
return "Empty"; //❌,"Empty"也是一个局部临时量,并且以引用的方式传出,在函数结束调用后该临时量会被销毁,使得外部调用点的引用不再绑定具体对象
}
}
//以下写法严重错误,不能返回一个指向局部对象的指针
int *funcIp(int var) {
int i = 10;
int *ip = &(i + var); //i + var对象是一个临时量,结束调用后销毁,对外返回的指针无法指向确定的内存
return ip;
}
2.3 返回类类型的函数可以连续调用
返回类类型(类的对象或其引用,或指向类的对象的指针)的函数,可以在其调用处继续使用调用运算符去调用该类的其他函数,如下例:
const string &shorterStr(const string &s1, const string &s2) { //此函数返回的是一个string对象的引用
return s1.size() <= s2.size() ? s1 : s2;
}
string s1 = "aaaa";
string s2 = "bbb";
auto sz = shorterStr(s1, s2).size(); //shoterStr(s1, s2)返回的是一个string对象(s2),该对象有size()成员函数,因此可以在shoterStr()的返回处再继续调用size(),这个调用结束后,返回的是一个size_t类型值对象,由auto类型对象sz接收
2.4 引用返回左值
调用一个返回引用的函数,在调用处得到左值(左值可以放在赋值号左边用来被赋值)调用返回其他类型的函数,在调用处得到右值(右值可以放在赋值号右边用来赋值)可以像使用其他左值那样来使用返回左值(返回引用)的函数调用,特别是能为返回类型是非常量引用的函数结果进行赋值。如下例:但如果函数的返回值类型是常量引用,就不能给调用结果进行赋值了,这是常量特性所决定的
char &get_val(string &str, string::size_type ix) { //该函数的返回值类型是非常量引用,其返回结果可以在调用处被当作左值
return str[ix]; //假定索引值ix一定合法
}
const string &shorterStr(const string &s1, const string &s2) { //此函数返回的是常量引用
return s1.size() <= s2.size() ? s1 : s2;
}
int main() {
string s("a value");
cout << s << endl; //输出:a value
get_val(s, 0) = 'A'; //get_val的返回结果在此处被当作左值,因为它的返回值类型是非常量引用,可以完成这里的赋值操作,将s[0]的值改为A
cout << s << endl; //输出:A value
shoterStr("hi", "bye") = "X"; //❌,返回的是常量引用(也就是常量对象),不能被修改,因此也不能这样作为左值使用
return 0;
}
2.5 返回数组指针
由于数组不能被拷贝的特点,函数不能直接返回数组本身。但函数可以通过返回指向数组的指针或绑定数组的引用来间接地返回数组。但这回带来一个问题,就是在函数签名中写这种返回值类型会非常繁琐。如下所述:
2.5.1 用一般方式声明一个返回数组指针的函数
先看一个例子:
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10]; //p1是一个含有10个指向整数的指针的数组
int (*p2)[10] = &arr; //p2是一个指针,它指向含有10个整数的数组arr
类似以上的声明案例,如果想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,然而函数的形参列表也跟在函数名字之后,且形参列表放在数组维度之前,因此,返回数组指针的函数签名如下:
Type (*funcName(parameter_list)) [dimension]
Type 返回的数组中元素的类型demension 返回数组的大小funcName 函数名parameter_list 参数列表
类似上个案例中的情况,(*funcName(parameter_list)) 两端的括号必须存在,否则函数的返回类型将是存储指针的数组,例如:
int (*func(int i))[10]; //按以下顺序逐层理解该声明的含义
// func(int i) 表示调用func函数需要一个int类型的实参
// (*func(int i)) 表明可以对函数的调用结果执行解引用操作(函数的调用结果是个指针)
// (*func(int i))[10] 表明对func的调用结果解引用,得到的是一个大小为10的数组
// int (*func(int i))[10] 表明这个数组中的元素类型是int
这种写法非常冗杂,建议使用另外两种写法。
2.5.2 使用类型别名来简化
typedef int arrT[10]; //使用typedef进行类型别名的定义,声明arrT是一个类型别名,他表示的类型是【含有10个整数的数组】类型
using arrT = int[10]; //使用using进行类型别名的定义,含义与上一行代码等价
arrT *func(int i); //使用已声明的类型别名arrT来指代【含有10个整数的数组】类型,表明func返回一个指向arrT类型(含有10个整数的数组)的指针
2.5.3 使用尾置返回类型(C++11新特性)来简化
任何函数的定义都可以使用尾置返回,但主要使用这种方法简化返回值类型比较复杂的函数定义,比如返回类型是数组的指针或数组的引用。格式如下:
auto funcName(parameter_list) -> returnType
将函数真正的返回类型放在形参列表之后的 -> 符号后,用auto在本应出现返回类型的地方占位。
例如:
// func接受一个int类型的实参,返回一个指向含有10个整数的数组的指针
auto func(int i) -> int (*)[10];
3 重载
重载的基本定义:
函数名相同,返回类型相同,参数列表不同
3.1 重载和const形参
介绍参数传递时强调过,顶层const不影响传入函数的对象,即,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开。
另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象(区分底层const)可以实现重载。
例如:
int func1(int a);
int func1(const int a); //❌,无法区分顶层const形参和无顶层const形参,并没有实现重载,而是重复定义了int func1(int a)
int func2(int *p); //p是指向int类型对象的普通指针
int func2(int * const p); //p是指向int类型对象的const指针,其指向不能改变
//以上行也❌,无法区分顶层const形参(const指针p)和无顶层const形参(普通指针p),没有实现重载
//对于接收引用或指针的函数来说,其指向的对象是常量还是非常量(区分底层const)对应的形参是不同的
int func3(int &r); //形参是绑定int对象的引用
int func3(const int &r); //形参是绑定const int对象的引用,这是一个新函数,重载成功
int func4(int *p); //形参是指向int对象的指针
int func4(const int *p); //形参是指向const int对象的指针,这是一个新函数,重载成功
3.2 重载与作用域
作用域的性质是:在内层作用域中声明的名字,将隐藏外层作用域中声明的同名实体。因此,不能重载不同作用域中的函数名。



