函数
局部对象
局部变量会隐藏外层作用域中同名的其他所有声明。
局部静态对象
在程序的执行路径第一次经过对象定义语句时初始化,并直到程序终止才会被销毁,即使对象所在的函数结束执行也不会对它有影响。
如果局部静态变量没有显式的初始化,它将会被进行值初始化,内置类型的局部静态变量初始化为0.
参数传递
传值参数
初始化一个非引用类型的变量时,初始值被拷贝给变量,因此对变量的改动不会影响初始值。
指针形参
传递指针形参时,拷贝的是指针的值,也就是地址。这是两个不同的指针。通过指针可以间接访问所指向的对象并修改对象的值。
int n = 0, i = 2;
int *p = &n, *q = &i;
*p = 2; //n的值变为2
p = q; //p指向了i
void reset(int *ip){
*ip = 0;//改变了ip所指向的对象的值,也就是i的值
ip = 0; //只改变了ip的局部拷贝,实参未被改变
}
int i = 42;
reset(&i);
cout << i << endl; //输出结果是i=0.
传引用参数
使用引用传递参数可以避免拷贝,减小计算消耗。此外,还有一些类类型不支持拷贝操作(IO类型等),只能通过引用形参访问该类型对象。
bool isShorter(const string &s1, const string &s2){ //若无需改变引用形参的值,最好将其声明为常量引用
return s1.size() < s2.size();
}
const形参和实参
顶层const作用于对象本身。底层const作用于对象(通常指针)所指向的对象。
与其他初始化一样,用实参初始化形参时,会忽略掉顶层const。也就是说形参有顶层const时,传给他常量对象或者非常量对象都是可以的。
忽略顶层const可能会产生一些意想不到的问题。
void func(const int i){...} //func能读取i,但不能写
void func(int i){...} //错误,重复定义了func,因为顶层const被忽略了。
数组形参
数组的特殊性质:
- 不允许拷贝数组
- 使用数组会将其转换成指针
所以向一个函数传递数组时,其实传递的是数组首元素的指针。
//三种等价形式,其形参都是const int *
void print(const int *); //
void print(const int[]); //
void print(const int[10]); //这里的维度表示的是我们希望数组有多少维度,实际不一定
数组引用形参
void print(int (&arr)[10]){
for(auto elem : arr){
cout << elem << endl;
}
}
int &arr[10]; //这是一个包含10个引用的数组
int (&arr)[10]; //这是一个指向包含10个整数的整型数组的引用
main处理命令行参数
int main(int argc, char *agrv[]){...} //argv是一个数组,其中的每个元素是指向一个C风格字符串的指针, argc表示数组中字符串的数量
//等价于
int main(int argc, char **agrv){...}
argv[0]是程序的名字,可选的实参从argv[1]开始。
含有可变形参的函数
- 如果所有形参类型相同,可以传递一个名为initializer_list的标准库类型。
- 如果实参类型不一样,可以编写可变参数模板来实现可变形参的函数。
- 特殊形参类型,即省略符。一般只用于与C函数的交互接口程序。
initializer_list
initializer_list定义在同名头文件中。
initializer_list提供的操作 | |
---|---|
initializer_list lst; | 默认初始化T类型的空列表 |
initializer_lsit lst{a, b, c, …}; | lst的元素和初始化列表一样多,lst元素是对应初始值的拷贝,列表中的元素是const. |
lst2(lst); lst2 = lst | 拷贝或者赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后原始列表和副本共享元素 |
lst.size(); | lst中的元素数量 |
lst.begin(); | 返回指向lst的首元素指针 |
lst.end(); | 返回指向lst尾元素后一个位置的指针。 |
void error_msg(initializer_list<string> il){
for (auto beg = il.begin(); beg != il.end(); ++beg){
cout << *beg << endl;
}
}
string expect = "hhh";
string actual;
cin >> actual;
if(expect != actual){
//如果向initializer_list形参中传递一个值的序列,必须把序列放在一个花括号内。
error_msg({"functionX", expect, actual});
}
else
{
error_msg({"functionX", "ok"});
}
当然,含有initializer_list的函数还可以拥有其他形参。
省略符形参
省略符形参会调用varargs的C标准库功能。…只能用于C和C++都通用的类型,对于大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后位置。
void f(param_list, ...);
不要返回局部对象的引用或者指针。
因为函数返回后,局部对象(静态局部变量除外)的存储空间就会被释放掉,也就是说局部变量的引用将指向无效的地址。
返回类类型的函数和调用运算符
如果函数返回的是指针、引用或者类的对象,那么可以使用函数调用的结果来访问对象的成员。
//调用返回的string对象的size成员
auto sz = shorterString(s1, s2).size();
列表初始化返回值
C++11新标准中,函数可以返回花括号包围的值的列表。
可以像这样:
vector<string> process(){
//expected, actual是string对象
if(expect.empty()){
return {};
}
else if (expect == actual){
return {"functionX", "ok"};
}
else{
return {"functionX", expect, actual};
}
}
如果函数返回的是内置类型,则花括号包围的列表最多包含一个值。
返回数组指针
返回函数指针主要有四种方法:一般的声明方式、使用类型别名、使用尾置返回类型以及使用decltype。
使用类型别名
typedef int arrT[10]; //arrT是类型别名,表示的是含有10个整数的数组
using arrT = int [10]; //等价形式
arrT* func(int i); //返回指向含有10个整数的数组的指针。
一般的声明方式
如果要定义返回数组指针的函数,数组的维度必须跟在函数名字之后,且放在形参列表之后。
type(*function(parameter_list))[dimension]
int (*func(int i))[10]; //返回指向包含10个int元素的数组的指针
int *func(int i)[10]; //没有外层括号,返回的则是包含10个int指针的数组
使用尾置返回类型
使用尾置返回类型可以简化上述声明。
//表明函数接收一个int实参,返回一个指针,该指针指向含有10个整数的的数组
auto func(int i)->int (*)[10];
使用decltype
若直到函数返回的指针指向哪个数组,就可以使用decltype声明返回类型。
int odd[] = {1, 3, 5};
int even[] = {2, 4, 6};
decltype(odd) *arrPtr(int i){ //decltype不负责把数组类型转化成对应指针,所以arrPtr前面要加上*
return (i % 2) ? &even : &odd;
}
小练习:编写一个函数声明,使其返回数组的引用,该数组包含10个string对象。
string (&func(int i))[10];
//使用类型别名
using s10 = string [10];//等价于typedef string s10[10];
s10 &func(int i);
//尾置返回类型
auto func(int i)->string (&)[10];//这样理解,首先是一个引用&,这个引用绑定到string[10]数组中。
//s使用decltype
string s[10];
decltype(s) &func(int i);
main函数的返回值
main函数允许没有return语句直接结束,编译器会隐式地插入一条返回0的语句。
函数递归调用消耗栈空间。main函数不能调用自己。
函数重载
main函数不能重载。
一个作用域内,函数名字相同,形参列表、返回值不同则是函数重载。
const形参的问题
顶层const不影响传入函数的对象,也就是说无法将含有顶层const的形参和不含有顶层const的形参区分开来。
int func(int i);
int func(const int i);// 重复声明了func
int ptr(int* p);
int ptr(int* const p); //重复声明了ptr
对于接收引用和指针的函数来说,对象是常量还是非常量,对应的形参不同
int fun(int *p);//函数作用于指向int的指针
int fun(const int *p); //新函数,作用域指向常量的指针
int ref(int &i); //函数作用于int引用
int ref(const int &i);//新函数,作用于常量引用
当传递一个非常量对象或者指向非常量对象的指针时,会优先调用非常量版本函数。
const_cast和重载
const string &shorterString(const string& s1, const string& s2){
return s1.size() < s2.size() ? s1 : s2;
}
参数和返回类型都是const string&。函数传入两个非常量string后,得到的是const string的引用,这样一来,s1和s2就不可写了,这会影响后面程序。所以,需要一个新的函数,在它的实参不是常量时,得到的是一个非常量引用。
string shorterString(string &s1, string &s2){
auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
return const_cast<string&>(r);
}
这样一来,传入2个非常量string,比较得到较短的对象的非常量引用,这样显然是安全的。
在C++中,名字查找发生在类型检查之前。
默认实参
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
一旦某个形参被赋予了默认值,后面的所有形参必须有默认值。
默认实参声明
在给定作用域中,一个形参只能被赋予一次默认实参。
//表示高度和宽度没有默认实参
string screen(sz, sz, char = ' ');
//不能修改一个已经存在的默认值
string screen(sz, sz, char = '*');//错误,重复声明了
//但是可以像下面这样添加默认实参,因为第三个参数已经有了默认实参,所以符合之前的标准。
string screen(sz = 24, sz = 40, char);
默认实参初始值
局部变量不能作为默认实参。
//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, '*')
}
内联函数
内联函数可以避免函数调用的开销。
inline const string & shorterString(const string & s1, const string &s2){
return s1.size() < s2.size() ? s1 : s2;
}
constexpr函数
函数的返回类型和所有形参类型都是字面值类型,函数体中必须有且只有一条return语句。
constexpr int new_sz(){return 42;}
constexpr int foo = new_sz(); //正确,foo是一个常量表达式
编译器在编译时能够验证函数的返回值类型。
constexpr函数的返回值不一定是一个常量。
//如果arg是常量表达式,则scale(arg)也是常量表达式,反之则不然
constexpr size_t scale(size_t cnt){
return new_sz() * cnt;
}
int arr[scale(2)]; //正确,scale(2)是常量表达式
int i = 2;
int arr2[scale(i)]; //错误,scale(i)不是常量表达式
一般情况下,把内联函数和constexpr函数放在头文件中。
调试帮助
assert预处理宏,定义在cassert头文件中。
string s("hello");
assert(s.size() > 5);
assert的行为依赖于NDEBUG预处理变量的状态。如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG,assert将执行运行时的检查。
CC -D NDEBUG main.c
等价于在main.c文件开始写#define NDEBUG。
void print(const int a[], size_t size){
#ifndef NDEBUG
//__func__是编译器定义的一个局部静态变量,用于存放函数名字,是一个const char的静态数组。
cerr << __func__ << endl;
#endif
...
}
- __ FILE __ 存放文件名的字符串字面值
- __ LINE __ 存放当前行号的整型字面值
- __ TIME __ 存放文件编译时间的字符串字面值
- __ DATE __ 存放文件编译日期的字符串字面值
函数匹配
const实参
如果重载函数的区别是形参的引用类型是否引用了const,或者指针的形参是否指向了const,则调用时编译器通过实参是否是常量来决定使用哪个函数,如下:
int f(int &i);
int f(const int &i);
const int a = 0;
int b = 1;
f(a); //调用f(const int &i),不能把普通引用绑定到const对象上。
f(b); //调用f(int &i)
函数指针
- 函数的类型由其返回值类型和形参类型共同决定。
- 函数是不能直接返回函数的,也不能直接返回数组。(只能返回函数指针或数组指针。)
bool shorterString(const string&, const string&);
该函数类型是bool (const string&, const string&)。声明一个函数指针,则只需要将函数名替换成指针即可。
//未初始化的指针
bool (*pf)(const string&, const string&) //指针的括号不能省,如果省略,pf就是一个返回值为bool*的函数
把函数名作为一个值使用时,跟数组一样,都会自动地转换成指针。
pf = shorterString;
pf = &shorterString; //两种形式等价
函数指针可以调用该函数,无需提前解引用指针。
bool b1 = pf("hello", "wolrd!");
bool b2 = (*pf)("hello", "world!");//等价形式
bool b3 = shorterString("hello", "world!");
指向不同函数类型的指针不存在转换规则。也就是说函数指针指向的函数的类型必须与指针类型一致。
重载函数的指针
指针类型必须与重载的函数中的某一个类型精确匹配。
函数指针形参
与数组类似,不能直接定义函数类型的形参,但是可以定义指向函数的指针类型的形参。
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 &));
//还可以直接把函数当作实参使用,此时函数会被转换成函数指针。
useBigger(s1, s2, shorterString);
返回指向函数的指针
和数组类似,虽然不能直接返回函数,但可以返回指向函数的指针。
但是编译器不会自动将函数返回类型当成对应的指针类型处理。简便方式就是使用类型别名来声明返回函数指针的函数。
using F = int(int*, int); //F是函数类型
using PF = int(*)(int*, int); //PF是指针类型
PF f(int); //正确,PF是指向函数的指针,f1返回函数指针
F f(int); //错误,F是函数类型,而函数f1不能返回一个函数
F* f(int); //正确,显式地指定返回类型是指向函数的指针。
使用尾置返回类型声明返回函数指针的函数。
auto f(int)->int(*)(int*, int);
还可以直接声明:
int(*f(int)) (int*, int); //f(int)函数返回一个指向int(int*, int)类型函数的指针。
将auto和decltype用于函数指针类型
如果明确知道返回的函数是哪一个,就可以使用decltype简化书写函数指针返回类型的过程。
string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
decltype(sumLength) *getFcn(const string&);
时刻注意decltype()作用于函数时,返回的是函数类型,而非指针类型,所以需要在getFcn前加上*表示函数的返回值是函数指针。
还有,decltype(expr)作用于表达式时,只是推断表达式的类型,而不实际计算其运算结果。