6.1 函数基础
- 函数的返回类型不能是数组或函数,但是可以是指向数组或者函数的指针。
6.1.1 局部对象
- 块是一个局部作用域,会隐藏块外的同名局部变量
- 在所有函数体外定义的对象在程序启动时创建,在程序结束时销毁。
- 局部对象在定义语句时创建,在块末尾销毁。
- 自动对象的初始化
- 变量定义时含有初值,用该初始值进行初始化
- 否则进行默认初始化,可能产生未定义的值。
- 局部静态对象,在程序第一次经过对象定义语句时初始化,直到程序终止才销毁。
6.1.2 函数声明
- 函数放在头文件
能保证同意函数所有的声明一致,需要修改时,修改一条声明就好;放在源文件中合法,但是容易出错。 - 含有函数声明的头文件应该包含到源文件中。
6.1.3分离式编译
- 分离式编译可以在修改单独某一个文件时使用。
如g++ fact.cpp factMain.cpp
生成一个a.out
。修改factMain.cpp中的内容后,需要重新使用该命令全部重新编译链接。
实际上,可以分成编译、链接两部分
g++ -c fact.cpp #编译,生成fact.o
#若修改factMain.cpp执行以下三步就可以了
g++ -c factMain.cpp #编译,生成factMain.o
g++ fact.o factMain.o #链接,生成a.out可执行文件
g++ fact.o factMain.o -o main #链接,生成main可执行文件
6.2 函数传递
- 形参是引用类型时,实参被引用传递(或函数被传引用)调用
- 实参值需要拷贝给形参时,实参被值传递(或函数被传值)调用。形参和实参相互独立,在函数块内拷贝构造实参(靠拷构造在第13章)。
6.2.1 传值参数
c++中建议使用引用类型代替指针
6.2.2 传引用参数
- 使用引用避免值拷贝
因为拷贝大的类在不必要地消耗资源,并且有的类并不支持拷贝操作,如iostream - 如果函数无须改变引用形参的值,最好声明为常量引用
- 使用引用实参可以返回额外信息
6.2.3 const形参和实参
- 当实参初始化形参时,会忽略掉顶层const,但是会保留底层const。
即若传入的指针(或引用)是顶层const的,其原来不能重新指向(或表示)其他变量,在函数体内没有这个限制,可以重新指向(或表示)其他变量。 - 可以使用非常量初始化一个底层const对象,但是反过来不行
- 在声明函数时,尽量使用常量引用
把不会改变的形参定义为普通引用是一个错误,会误导函数调用者这个函数会改变实参的值,并且会限制函数接受的实参类型
int find_char(string& s);
string s("无限~");
const string cs("罗小黑");
find_char(s);//可以调用
find_char(cs);//不能用const初始化一个非常量,编译时报错
find_char2(const string& s);
find_char2(s);
find_char2(cs);//都可以正常调用
6.2.4 数组形参
- 数组以指针的形式传递给函数。
即使在函数内,程序员也要保证数组不能越界,可以使用三种方法告诉函数数组的大小。- 数组本身含有一个标记符,如字符数组可以以
\0
为标记。 - 以数组首元素和尾后元素指针为参数
- 传递一个表示数组大小的形参
- 数组本身含有一个标记符,如字符数组可以以
//这种形式是允许的,但是也是危险的,编译器会把ia转换为指针类型
//在传入大小不为10的数组不会报错
void print(const int ia[10]){
for (size_t i = 0;i<10;i++){
cout << ia[i] << endl;
}
}
int main(){
int a[5]={1,2,3,4,5};
print(a);//情况未知
}
- 允许函数参数定义数组的引用,数组维度也是参数类型的一部分
//函数只能作用于大小为10的int数组
//&arr两侧必须加上括号,int &arr[10]是引用的数组,而非数组的引用
void fun(int (&arr)[10])
- 若需要将多维数组作为函数参数,函数第二维(及以后所有维度)都是数组的一部分,不能省略。
//matrix是指向含有10个整数的数组的指针
void fun(int (*matrix)[10],int rowsize);
//等价定义
void fun(int matrix[][10],int rowsize);
6.2.5 main处理命令行选项
有时需要给main传实参,如告诉函数将要执行的操作。
假设程序在可执行文件prog中,希望向程序输入以下选项
prog -d -o ofile data0
main可以声明为以下的形式
int main(int argc,char** argv);
argv是一个数组,元素是c风格字符串指针
argc是argv中元素个数。
若输入命令是prog -d -o ofile data0
,argc为5,argv[0]到argv[4]依次是prog
-d
-o
ofile
data0
。argv[5]为0。
argv[0]是程序名,argv[1]才是可用实参
6.2.6 含有可变形参的函数
- 两种主要方法,一种不常用方法来处理不同数量的实参的函数
- 主要方法1:实参类型相同,使用标准库类型,initializer_list
- 主要方法2:实参类型不同,可变参数模板 (16.4节详讲)
- 非常用:省略符形参,只用于与c函数交互的接口程序
- initializer_list
- initializer_list对象中的元素永远是常量
- 省略符形参
- 省略符形参是为了访问某些特殊的c代码,这些代码使用了名为
varargs
的c标准库功能。 - 省略符形参仅用于c和c++通用的类型,并且大多数类类型在传递给省略符形参时都无法正确拷贝
- 形式有以下两种
- 省略符形参是为了访问某些特殊的c代码,这些代码使用了名为
void foo(para_list,...);//指定了部分形参类型,对指定类型会执行正常的类型检查,且逗号符是可选的
void foo(...);//省略符形参对应的实参无须类型检查
6.3 返回类型和return语句
6.3.1 无返回值函数
可以使用return;
或者 return void;
6.3.2 有返回值的函数
- 在含有return语句的循环语句后面也应该有一条return语句,如果没有的话,这个程序就是错误的。但是很多编译器都无法发现这个错误。
bool cmp_test(const string& str1,const string& str2){
auto size = str1.size()<str2.size()?str1.size():str2.size();
for (decltype(size) i = 0;i<size;++i){
if(str1[i]!=str2[i]){
return ;//错误,但编译器可能发现不了
}
}
}
- 不要返回局部对象的引用和指针或者返回未定义的的值。
在返回时,一般是返回值的拷贝,这是安全的;但仅仅返回一个指向对局部对象的指针或引用的拷贝,这就是危险的。 - 调用运算符的优先级和点运算符与箭头运算符优先级相同,并且符合左结合律。
所以可以直接调用返回指针或类的对象
shorterString(s1,s2).size()
- 返回一个引用的函数得到左值,其他类型得到右值
- 可以使用初始化列表返回值
vector<string> fun(){
//使用返回的初始化列表初始化vector
return {"string1","string2","string3"};
}
- main的返回值被看作状态指示器,返回0表示成功,其他表示失败。
- 非0值的具体含义依机器而定
- main函数不可以递归调用自己
- cstdlib头文件中定义了预处理变量表示成功失败
int main(){
if(some_fail) return EXIT_FAILURE;
else return EXIT_SUCCESS;
}
6.3.3 返回函数指针
- 定义一个返回数组指针的函数,数组的维度必须跟在函数名字之后
//函数定义
Type (*function(parameter_list))[dimension]
//fun这个函数返回数组指针,这个指针指向有10个int类型的数组
int (*fun(int i ))[10];
//fun(int i ) 表示函数需要一个int型实参
//(*fun(int i )) 表示可以对函数进行解引用操作
//(*fun(int i ))[10] 表示引用操作得到一个大小为10的数组
//int (*fun(int i ))[10]表示数组中元素为int类型
- 使用尾置返回类型
auto fun(int i)->int(*)[10]
- 使用decltype
int arr[]={0,1,2,3,4,5,6,7,8,9}
decltype(arr) *fun(int i);
6.4 函数重载
- 重载函数:函数名字相同,形参列表不同。(无法识别不同的返回值)
- main函数不能重载
- 顶层const形参无法和另一个没有顶层const的形参区别开,但是可以识别底层const(形参时指针或引用,可以区分其指向常量对象和非常量对象实现重载)
Record lookup(Phone);
Record lookup(const Phone);//无法和Record lookup(Phone)区分
Record lookup(Phone*);
Record lookup(const Phone*);//正确,与Record lookup(Phone*)不同
- 最好只重载那些非常相似的操作。
- const_cast在函数中的使用
const string& shorterString(const string& s1,const string& s2){
return s1.size()<+s2.size()?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);
}
- 函数调用的三种结果
- 编译器找到实参的最佳匹配,生成该函数的代码。
- 找不到合适的实参的最佳匹配,编译器法抛出无匹配的错误
- 有多于一个函数可以匹配,但每一个都不是最佳选择,编译器发出
二义性调用
错误
6.4.1 重载与作用域
- 将函数声明放在局部作用域中很不明智,以下只是说明c++特性,并不建议使用
在不同作用域中,函数就不是被重载而是隐藏了
void print(string str);
int main(){
//在新作用域中声明不好
void print(int ival);
//错误,因为string 为参数的函数已经被隐藏
print("ni hao");
}
6.5 特殊用途语言特性
6.5.1 默认实参
- 一旦某个形式被赋予了默认值,它后面的所有形参都必须要有默认值
- 通常,应该在函数声明中指定默认实参,并将该声明放到合适的头文件中
- 局部变量不能作为默认实参。只要表达式的类型能够转换成形参所需的类型,这个表达式就能作为默认实参。
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,'*')
}
6.5.2 内联函数和constexpr函数
- 内联函数
- 内联函数会在调用点展开,避免函数调用的开销
- 在函数返回值前加上inline就可以实现
- 内联说明只是向编译器发出一个请求,编译器可以忽略这个请求
- 内联机制一般用于优化规模较小、流程直接、调用频繁的函数。
- 很多编译器不支持内敛递归函数
- constexpr函数
- constexpr函数是指能用于常量表达式的函数
- constexpr函数需要遵守的要求:
- 返回值类型及所有形参的类型都是字面值类型
- 函数体内有且只有一条返回语句。
- 若存在其他语句,这些语句不能执行任何操作,如空语句、类型别名、using声明。
- constexpr函数隐式被指定为内联函数
- 内联函数和constexpr函数放在头文件
6.5.3 调试帮助
可以使用assert
和NDEBUG
的预处理功能来调试代码
- assert预处理宏
- assert(expr),若expr为真,assert啥也不做;否则,assert输出信息并终止程序执行。
- assert宏定义在
cassert
头文件中 - 预处理名字由预处理器而不是编译器管理,因此可以不用using声明
- 含有
cassert
头文件的程序,不能使用名为assert的变量、函数或其他实体。
即使没有直接包含cassert头文件,也有可能间接包含了该头文件,因此最好不要使用名为assert的变量、函数或其他实体
- NDEBUG预处理变量
- 定义了 NDEBUG,则
assert
什么都不做 - 使用
NDEBUG
的方法- 在程序中使用
#define NDEBUG
- 在编译时加入命令,如
CC -D NDEBUG main.cpp
- 在程序中使用
- 自己使用NDEBUG 编写条件调试
- 定义了 NDEBUG,则
void print(){
#ifndef NDEBUG
//__func__是编译器定义的静态局部常量,用来存放函数名字
cerr<<__func__<<"debugging"<<endl;
#endif
...
}
- 编译器除了定义
__func__
外,还定义了如下变量__FILE__
存放文件名的字符串字面值__LINE__
存放当前行号的整型字面值__TIME__
存放文件编译时间的字符串字面值__DATE__
存放文件编日期时间的字符串字面值
6.6 函数匹配
- 第一步:确定候选函数。候选函数与被调用函数同名、在调用点可见。
- 第二步:确定可行函数,可行函数形参和本次调用提供的实参数量相当、每个实参的类型与对应的形参类型相同或能转换成形参类型
- 第三步:确定最佳匹配
在调用重载函数时,应该尽量避免强制类型转换。若需要强制类型转换,说明设计的形参集合设计不合理。
6.6.1 实参类型转换
- 实参到类型的转换分为五个等级
- 精确匹配
- 通过const转换实现的匹配
- 通过类型提升实现的匹配
- 通过算术类型转换或指针转换实现的匹配
- 通过类类型转换实现的匹配
6.7函数指针
- 函数指针指向的是函数非不是对象,其类型由返回类型和形参类型确定,与函数名无关。
- 函数指针声明:用指针替换函数名即可
//函数声明
bool lenghthCmp(const string& s1,const string& s2);
//函数指针声明,(*pf)的括号不可少,否则就是返回指向bool的指针
bool (*pf)(const string& s1,const string& s2);
//以下赋值等价,取地址符时可选的
//但是函数原型、返回值要和pf声明完全一致,精确匹配
pf = lenghthCmp;
pf = &lenghthCmp;
//以下都是等价的调用
bool b1=pf("hello","goodbye");
bool b2=(*pf)("hello","goodbye");
bool b1=lenghthCmp("hello","goodbye");
- 函数指针可以作为函数的参数,实参可以直接使用函数名。函数作为参数会隐式转换为函数指针。
- 可以使用decltype、类型别名来简化声明函数指针
//相同的声明
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,lenghthCmp)
//fun1 和fun2 是函数类型
typedef bool fun1(const string &,const string &);
typedef decltype(lenghthCmp) fun2;
//funp1 和funp1 是指针类型
typedef bool (* funp1)(const string &,const string &);
typedef decltype(lenghthCmp) *funp1;
- 返回指向函数的指针
函数指针作为返回类型和作为参数不一样。
函数作为参数会自动转为指针,但作为返回值不会自动转换
using F = bool(const string &,const string &);//函数类型
using Fp = bool(*)(const string &,const string &)//指针类型,(*)的括号也是必要的
//也可以用尾置返回类型
auto Fp2(const string &,const string &)->bool(*)(const string &,const string &);