一、函数声明
函数和全局变量一样,可以一次定义,多次声明从而可以在多个文件中使用。
函数的声明也叫函数的原型(function prototype)。
// fact.h:头文件
int fact(int, int); // 函数声明
// fact.cc:源文件
#include "fact.h" // 源文件中包含头文件
int fact(int a, int b) { // 函数定义
return a + b;
}
返回类型 函数名字(类型1 形参1,类型2 形参2,...,类型n 形参n) {
函数体
}
int fuck(int a){ // a:形参
int b = 1; // 局部变量
cout << a + b << endl;
return a + b;
// return语句完成两项工作:
// 1、返回return语句中的值。
// 2、将控制权从被调函数转移回主调函数。
}
// 通过调用运算符(),来执行函数。
fuck(100); // 1、实参100初始化函数形参a
// 2、控制权转移给被调函数,主调函数执行中断
void func(const int i) {} // func 能够读取 i, 但是不能向 i 写值
void func(int i) {} // 错误:重复定义了 func(int)
函数的调用完成两项工作:
- 引用传递
- 形参是引用类型,是对实参的引用,对形参的操作就是对实参的操作,使用引用传递可以避免值拷贝代价。
- 值传递
int fact(int a, int b){ // 形参列表
return a + b;
}
int fact( ){ // 隐式定义空形参列表
}
int fact(void){ // 显示定义空形参列表(为了与C兼容)
}
fact(10); // 实参10初始化形参a。
const形参
const形参的初始化和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 = O;
const int ci = i;
string::size_type ctr = O;
reset(&i); // 调用形 参类型是 int*的 reset 函数
reset(&ci); // 错误:不能用指向 const int 对象的指针初始化 int* '
reset(i); // 调用形参类型是 int& 的 reset函数
reset(ci); // 错误:不能把普通引用绑定到 const 对象 ci 上
reset(42); // 错误:不能把普通应用绑定到字面值
reset(ctr); // 错误:类型不匹配, ctr 是无符号类型
// 正确: find_char 的第一个形参是对常量的引用
find_char("Hello World! 11",'o', ctr);
数组形参
数组将以指针方式传递,指针指向数组首地址。
void print(const int*); // 等价
void print(const int[]); // 等价
void print(const int[10]); // 等价
// 如何确定数组形参的范围
void print(const char *cp); // C风格字符串,以空字符结束
void print(const int *beg, const int *end); // 标准库规范,首尾迭代器限定范围
void print(const int ia[], size_t size); // 显式指定数组大小
// 数组的引用,必须括号。
void print(int (&arr)[10]); // 数组的引用,限定了数组大小只能是10
// 多维数组形参
// 二维数组的指针,数组元素是10个int元素的数组。
void print(int (*matrix)[10], int rowSize); // 等价
void print(int matrix[][10], int rowSize ) // 等价
// main函数,命令行选项(多维数组)
// prog -d -o ofile data0,main函数会收到两个参数。
// 下面两种等价,注意argv[0]是程序名字,argv[1]才是ofile
int main (int argc , char *argv[]); // 数组元素是指针(C风格字符串)
int main (int argc , char **argv); // 数组也用指针传递。第二个*是数组的元素。
可变形参
可变参数列表。
这里考虑的情况是:数量未知、类型相同。
原理:动态类型数组,用数组来实现,只不过数组的元素类型是用模板来动态确定。
具体是用标准库类型initializer_list,某种特定类型值的数组。定义在头文件initializer_list中。注意!它的元素是常量值,不可改变。
/***************initializer_list提供的接口************/
initializer_list<T> lst; // 默认初始化:T类型元素的空列表
// lst的元素数量和初始值一样多;lst的元素是对应初始值的副本:列表中的元素是const
initializer_list<T> lst{a,b,c...};
lst2(lst) // 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素,
lst2 = lst // 拷贝后,原始列表和副本共享元素
lst.size() // 列表中的元素数量
lst.begin() // 返回指向 lst中首元素的指针
lst.end() // 返回指向 lst 中尾元素下一位置的指针
/********************可变形参例子************/
void error_msg(int num, initializer_list<string> args) // 使用的时候必须用花括号
{
for(auto beg = args.begin() ; beg != il.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
error_msg(0,{"fuck", "you", "mother", "fucker"}); // 必须用花括号括起来。
// C风格的省略符形参,尽量少在C++中使用。
void foo(parm_list,...); // 省略符形参这是C++为兼容C设置的。...必须在最后
void foo(...); // 使用了C标准库varargs
默认实参
typedef string::size_type size;
// 默认实参。
string screen( size ht= 24, size wid = 80, char backgrnd = '' );
screen(); // 等价
screen(24); // 等价
screen(24, 80); // 等价
screen(24, 80, ''); // 等价
screen(, ,'?'); // 错误,只能省略尾部的实参。
四、局部对象
名字有作用域,对象有生命周期。
局部变量:
自动对象:到底定义所在块的末尾时销毁,。
局部静态变量:local static object
形参和函数内部定义的变量,且会隐藏函数外部同名的变量。
int fact(int a) {
int b; // 自动对象:到底定义所在块的末尾时销毁。
// 执行默认初始化,因此内置类型默认都是未定值。
static int c; // 局部静态变量,进行指初始化,初始化为0。
// 多次调用只进行一次初始化。
return a;
}
五、返回值
void swap(int& a, int& b){
if(a == b)
return; // return void类型。
a ^= b;
b ^= a;
a ^= b;
return; // 此处return,可以省略,编译器会自动添加。
}
int fact(int a){
return a; // return expression类型。
}
int main(){
......
return 0; // 可省略,编译器会自动添加这条语句。
}
返回过程
两种返回情况
- 返回一个值,和初始化变量/形参的过程完全一样,只不过这个返回值初始化的是一个临时变量,在函数调用点的位置。
- 返回一个引用,没有初始化过程,就是返回某个变量的引用。
注意,不要返回局部变量的引用或指针。
string fuck(const string& word) {
return word; // 用word拷贝构造了一个临时的string对象在调用点:line 7
}
const string& fuckRef(const string& word){
return word; // 返回了word的引用,没有触发拷贝构造。
}
const string& errRef(){
string ret("asdf");
return ret; // 错误,返回了局部变量的引用。
}
int main(){
auto shit = fuck("asdfsadf"); // 整个过程调用了一次拷贝构造函数。
string str = "asdfas";
auto moth = fuckRef(str); // moth是str的引用,没有触发拷贝构造。
}
若返回引用,则返回的是一个左值;其他情况,返回的是一个右值。很好理解,返回的引用肯定是返回函数外部对象引用,而返回值则拷贝构造了一个临时变量,这是右值。右值可以理解为由编译器创建和销毁的变量。
返回列表初始化
可以返回一个列表,函数的返回类型就会进行列表初始化。可以实现多返回值的效果,不过类型必须全部相同。
返回vector,那列表的元素不限制。
返回内置类型,如int,那列表只能有一个元素。
vector<string> process(){
// expected 和 actual 是 string 对象
if (expected.empty())
return{}; //返回一个空vector对象
else if(expected == actual )
return{"functionX", "okay"}; //返回列表初始化的 vector 对象
else
return{"functionX", expected, actual } ;
}
int process(){
return {1}; // 返回类型是内置类型,列表中只能一个值。
}
返回数组指针
/***************类型别名来表示数组指针**********/
typedef int arrT[10]; // arrT 是一个类型别名,它表示的类型是含有 10 个整数的数组
using arrT = int[10]; // arrT 的等价声明, 参见 2.5. 1 节(笫 60 页)
arrT* func(int i) ; // func 返回一个指向含有 10 个整数的数组的指针
/***************函数返回数组指针的公式*****************/
Type (*function(parameter_list))[dimension]
int (*func(int i)) [10]; //例子:返回int[10]数组的指针。
//解读方法
//1、func(int i)表示调用 func 函数时需要一个 int 类型的实参。
//2、(*func(int i))意味着我们可以对函数调用的结果执行解引用操作 。
//3、(*func(int i))[10] 表示解引用 func 的调用将得到一个大小是10的数组。
//4、int(*func(int i)) [10] 表示数组中的元素是int类型 。
/***************使用 尾置返回类型 来返回数组指针*****************/
auto func(int i) -> int(*)[10]; // 前面用auto代替。
/***************使用 decltype 来返回数组指针*****************/
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arrPtr(int i); //返回一个指向数组的指针
六、函数重载(overload)
在相同作用域, 名字相同,形参列表不同的几个函数,叫重载函数。所以,重载的唯一关注点就是形参列表的差异,返回值类型不是考量范围。成员函数的this指针也属于参数列表的一员,因此引用限定符也可以重载。
以下是重载考量标准:
- 形参数量不同。
- 形参类型不同。
- 有无底层const
- const int *是底层const
- int * const是顶层const
- const int是顶层const
- 引用限定符。
以下不是重载考量标准:
- 返回值类型。
- 形参顶层const
- 也好理解,比如int const p的形参,其实int和int*const实参都可以传给这个形参,所以这个const并没有把形参区分开来。
void print(const char *cp); // 重载
void print(const int *beg, const int *end); // 重载:形参类型和数量不同
void print(const int ia[], size_t size); // 重载:形参类型和数量不同
Record lookup(Phone);
Record lookup(const Phone); // 错误,重复声明:顶层const不是重载。
Record lookup(Phone*);
Record lookup(Phone* const); // 错误,重复声明:顶层const不是重载。
Record lookup(Account&);
Record lookup(const Account&); // 重载:底层const
Record lookup(Account*);
Record lookup(const Account*); // 重载:底层const
//const_cast下面这种重载就非常有用。
const int& fuck(const int&, const int&);
int& fuck( int&, int&);
int& fuck(int& a, int& b){
// 这里直接调用了const int&重载版本的函数,这样就非常安全高效。
const int& ret = fuck(const_cast<const int&>(a), const_cast<const int&>(b));
return const_cast<const int&>(ret);
}
int fuck(double);
int fuck(int[]);
int fuck(string)
{
int fuck(int); // 在局部作用域定义,屏蔽了所有外部重载
fuck(1); // fuck(int)
fuck(1.2); // fuck(int),隐式转换
fuck("adf"); // 错误,匹配不到
}
函数匹配
/************函数匹配规则******************/
void f();
void f(int) ;
void f(int, int) ;
void f(double, double = 3.14);
f(5, 4.2); // 调用void f(double, double)
//1、确定重载函数集(同名、可见)
// 4个f函数
//2、实参和形参匹配
// 数量相等
// 类型匹配(精确、可转换)
// f( int, int )
// f( double, double )
//
//3、选出“最佳匹配”函数(实参和形参类型越接近,越好)
// a、 精确匹配
// 类型相同
// 数组、函数对应指针
// 向实参添加顶层const或者从实参删除底层const
// b、const转换实现匹配
// c、类型提升实现匹配
// d、算术类型转换、指针转换实现匹配
// e、类类型转换实现匹配
//
// f( int, int ),第一个int精确,第二个int要转换
// f( double, double ),第一个double要转换,第二个double精确
//
//4、匹配的函数不止有一个,编译器报错(二义性),否则就找出最佳匹配。
// f( int, int )
// f( double, double )
// 这两个都匹配,所以编译器报错。
/***************例子*********************/
void ff(int);
void ff(short) ;
ff('a'); // char提升成int;调用f(int)
void manip(long);
void manip(float);
manip(3.14); // 错误:二义性调用
Record lookup(Account&);
Record lookup(const Account&) ;
const Account a;
Account b;
lookup(a); // 调用 lookup (const Account&)
lookup(b); // 调用 lookup (Account&)
七、内联函数(inline)
设计目的:让一些高频使用的简单函数在调用处导入函数体代码,这样做可以避免函数调用的栈开销。
将函数声明为inline是告诉编译器,这个函数可以考虑内联,当然如果你故意让一个很复杂的函数内联,显然是不会成功的 。
inline const string & fuck(const string &sl, const string &s2 ){
return sl.size() <= s2.size() ? sl : s2
}
八、constexpr函数
能用于常量表达式的函数,参数和返回值都是字面值类型,且函数体有且只有一条return语句。
注意以上条件并不是必须的,如果不满足,就是一个普通函数。
一般放在头文件。
constexpr int new_sz() {
return 42 ;
}
constexpr int foo = new_sz (); // 正确:foo是一个常量表达式
// 如果cnt、new_sz是常量表达式,则scale是constexpr函数。
constexpr size_t scale(size_t cnt) { return new_sz() * cnt;)
int arr[scale(2)]; // 正确:scale(2)是常量表达式
int i = 2; // i不是常量表达式
int a2[scale(i)]; // 错误:scale(i)不是常量表达式
九、函数指针
指向函数地址的指针,类型由函数签名决定(返回类型、形参列表)。
函数指针间不存在转换,只有相同与不同。
函数指针对于函数必须是精确匹配(返回类型、形参类型、形参数量)
/*************函数指针格式***************/
// Type: 返回值类型
//funcptr: 函数指针名字,注意必须圆括号否则就是一个返回指针的函数
// 指针的&符、*符都是可选,也就是说
// funcptr = &func 等价于 funcptr = func
// funcptr() 等价于 (*funcptr)()
// ...: 参数列表
Type (*funcptr)(...);
/*************定义声明***************/
bool func(int); // 函数原型
bool (*pf)(int); // 函数指针,指向上面类型的函数
bool *fuck(int); // 不是函数指针,返回bool*类型的函数。
/*************赋值、初始化***************/
pf = func; // 等价,指向func
pf = &func; // 等价,&符可选
/*************调用***************/
bool bl = pf(1); // 等价
bool b2 = (*pf)(1); // 等价
bool b3 = func(1); // 等价
/*************作为参数***************/
void fuck(int, bool pf(int)); // 自动转成函数指针
void shit(int, bool (*pf)(int)); // 与上等价
fuck(1, func); // 函数自动转成指针。
shit(1, func);
/*************作为返回值***************/
// 数组不可复制、函数不能拷贝、但是函数指针可以复制。
int FuncProtoType(int);
using Func = int(int); // Func是函数
using FuncP = int(*)(int); // FuncP是函数指针
FuncP f1(int); // 正确
Func f1(int); // 错误:不能返回一个函数,这种情况,编译器不会自动转换。
Func *f1(int); // 正确
//同上,从里往外分析。
//1、f1(int),f1是一个函数
//2、*f1(int),f1返回一个指针
//3、int (*)(int),指针解引用,得到函数int()(int)
int (*f1(int))(int); // 等价
auto f1(int)->int(*)(int); // 等价
decltype(FuncProtoType) *f1(int); // 等价
/*************类型别名、decltype简化***************/
typedef bool Func(int); // Func是函数类型
typedef decltype(func) Func2; // 同上
typedef bool (*FuncP)(int) ; // FuncP是函数指针类型
typedef decltype(func) *FuncP2 ; // 同上
void ok(int, Func); // Func自动转成函数指针类型
void ok(int, FuncP2); // 同上
十、调试技巧
assert
预处理宏,类似内联函数,定义在assert头文件中,受预处理变量NDEBUG控制。注意!assert只是调试代码。
// 在C中,定义在<assert.h>中
// 在C++中,定义在<assert>中
#define NDEBUG // 关闭assert,在头文件之前定义,则assert将不生效。相当于生产环境
// 注释本句,则开启assert。
#include <assert>
assert(expr); // expr返回值为false,则立即终止程序。
// expr返回值为true ,则什么也不做。
NDEBUG
预处理变量,用于表示程序是否关闭调试状态。可以以此为开关编写自己的调试代码。
#define NDEBUG // 关闭调试状态,注释则开启调试状态。
void fuck(){
#ifndef NDEBUG // 程序调试代码
std::err << __func__ << std::endl; // 当前函数名字
std::err << __FILE__ << std::endl; // 文件名
std::err << __LINE__ << std::endl; // 当前行号
std::err << __TIME__ << std::endl; // 编译时间
std::err << __DATE__ << std::endl; // 编译日期。
#endif
}