学习目标:
函数
学习内容:
一、函数基础
-
函数就是一段命了名的代码块。一个函数通常包括四个部分:返回类型,函数名字,0个或多个形参列表以及函数体。
int fact(int val){ int ret = 1; while(val > 1){ ret *= val--; } return ret; }
-
调用函数的执行流程:1、使用实参初始化函数对应的形参;2、主调函数被中断,执行被调函数;3、函数执行完毕,返回return后的值;4、程序控制权从被调函数转移回主调函数。
int main(){ int j = fact(5); cout << "5! is " << j << endl; return 0; }
-
实参初始化形参的规则:1、形参和实参的个数必须一致;2、每个形参和实参的类型必须一致(允许隐式转换);
fact("hello"); // string和int类型不一致 fact(); // 实参太少 fact(42, 10); // 实参太多 fact(3.14); // 正确 double可以隐式转换为int
-
形参定义的规则:1、允许形参列表为空,通常用void;2、形参之间用逗号隔开,每个形参都必须有一个声明符,即使两个形参类型相同,也要把两个形参的类型都写出来;3、任意两个形参不能同名,函数内的变量名也不能和形参同名;
-
返回类型规则:1、允许不返回任何值,void;2、大部分类型都可以作为返回值;3、返回类型不能为数组或者函数类型,但是允许是指向数组或函数的指针;
-
局部变量:定义在形参和函数体内部的变量;
-
局部变量可以分为自动对象和局部静态对象;
i. 自动对象:只存在于块执行期间的对象,当执行函数时经过变量定义语句时创建对象,在函数执行结束时销毁它(形参属于自动对象);
ii. 局部静态对象(static局部变量):生命周期贯穿函数调用及之后的时间,当第一次执行函数经过变量定义时创建对象,直到主程序终止才销毁(对象所在函数执行结束,对象仍然存在)int count(){ static int cnt = 0; return ++cnt; } int main(){ // 静态局部变量 输出1~10 // 自动对象 输出10个1 for(int i = 0; i != 10; ++i){ cout << count() << endl; } return 0; }
-
函数声明
i. 函数声明和函数定义的唯一区别是函数声明没有函数体,直接用一个分号结束;
ii. 从函数声明中可以看出函数的三要素:返回类型、函数名、形参类型,函数声明描述了函数的接口;
iii. 函数声明也称为函数原型;
iv. 函数声明一般放在头文件,函数定义一般放在源文件; -
分离式编译
// 把main.cpp和fact.cpp一起编译成main_fact.exe g++ -g main.cpp fact.cpp -o main_fact
二、参数传递
- 实参初始化形参和变量的初始化方式一样,本质就是将实参的值拷贝给形参。
- 按照形参类型,可以将传参方式分为值传递和引用传递两种方式。
i. 引用传递:形参类型是引用类型,引用形参是它对应的实参的别名;
ii. 值传递:形参类型不是引用类型,指实参的值通过拷贝传递给形参;
1. 传值参数
-
形参是实参的副本,函数对形参做的所有操作不会影响实参本身;
i. 当形参是普通值类型:把实参(变量值)拷贝给形参int fact(int val){ int ret = 1; while(val > 1) ret *= val--; return ret; } int main(){ int a = 5; // fact(a) 把实参a的值拷贝给形参val cout << "5! is " << fact(a) << endl; // fact函数中改变了形参val的值,但是并不会影响实参a cout << a << endl; // 5 return 0; }
ii. 当形参是指针类型:把实参(变量的地址)拷贝给实参
void reset(int *p){ // 修改形参p 并不回影响实参(i的地址) *p = 0; // i = 0 p = 0; // 形参p指向0空指针 } int main(){ int i = 42; // &i = 0x61fe1c reset(&i); // 把实参(i的地址)拷贝给形参p 即形参p指向i cout << i << endl; // &i = 0x61fe1c return 0; }
iii. C++中建议使用引用类型的形参代替指针形参,指针形参最好不要用;
2.传引用参数
-
引用形参其实就是实参的一个别名,对形参改变,也会改变实参
void reset(int &p){ p = 0; } int main(){ int i = 42; // p是i的一个别名 对形参p操作 也会改变实参i reset(i); cout << i << endl; // 0 return 0; }
-
引用形参可以避免大拷贝:当传入的实参比较大,那么如果用普通的形参类型,它会将这个大实参拷贝给形参,效率比较低。相反使用引用的话,不需要拷贝,只是给这个大实参起了个别名而言,效率很高;
-
如果在函数内不需要修改引用形参的值,那么最好把它声明为常量引用const string &s;
-
使用引用形参还可以返回额外信息:一般一个函数只能返回一个值,如果需要返回多个值,那么就可以以引用的形式返回;
-
当实参是一个右值时,形参不能为引用类型;
// return返回s中第一次出现c的idx 引用形参times返回s中c出现的次数 // s可能很长,所以使用& 可以避免拷贝 不改变s所以是const // times需要返回给函数外 所以使用& // c 因为可能直接传入'a'字符 即实参可能是一个右值 所以不能用& int reset(const string &s, char c, int ×){ int idx = s.size(); for(int i = 0; i != s.size(); ++i){ if(s[i] == c){ if(idx == s.size()) idx = i; ++times; } } return idx; } int main(){ int times = 0; string s = "Hello World"; char c = 'l'; int idx = reset(s, c, times); cout << idx << " " << times << endl; // 2 3 return 0; }
3. const形参和实参
-
顶层const: 对象本身是一个const;底层const:指针指向的内容是一个const;
-
当执行对象的拷贝操作时,顶层const可以忽略,底层const必须一致;
const int ci = 42; // 顶层const int i = ci; // 正确 顶层const可以忽略 int *const p = &i; p = 0; // 错误 p(i的地址)是一个const 不能修改 *p = 0; // 正确 p不可修改 但是*P表示p指向的内容可以修改
-
而恰好实参初始化形参本质上就是将实参拷贝给形参;
// 这种函数重载是错误的,const是顶层const 可以忽略 所以两个函数是一样的 void fcn(const int i){} void fcn(int i){}
-
引用的const是底层const;
-
顶层const + & -> 底层const;
-
常量引用右边可以是任意东西(常量可以引用非常量):非常量/字面量/表达式/不同类型变量;
-
非常量不可以引用常量;
void reset(int *p){} // 1 void reset(int &p){} // 2 void reset(const int *p){} // 3 void reset(const int &p){} // 4 string::size_type find_char(string &s. char c, string::size_type &occurs); // 5 string::size_type find_char(const string &s. char c, string::size_type &occurs); // 6 int i = 0; const int ci = i; // 顶层 string::size_type ctr = 0; // unsigned int reset(&i); // 1 int *p = &i; reset(&ci); // 3 const int *p = &ci; &(顶层const) -> 底层const 所以不能选1 reset(i); // 2 int &p = i; reset(ci); // 4 const int &p = ci; 引用const是底层const 所以不能选2 reset(42); // 4 const int &p = 42; 常量引用右边可以是字面量 reset(ctr); // 4 常量引用右边可以是不太类型的变量 不能选2 类型不一样且无符号数无法自动转换为带符号数 find_char("hello world", 'l', ctr); // 6 字面值不是变量 不能对它取别名 但是常量引用右边却可以是一个字面值
-
对于函数中不会修改的形参要尽量设计成常量引用:常量引用右边接受的实参类型更多(字面值/常量/表达式/不太类型对象),不容易出错;
4.数组形参
-
不需要修改数组元素时,通常都将这个数组形参定义为const;
-
将数组形参定义为指向数组首元素的指针
i. 因为不能拷贝数组,所以无法通过值传递方式使用数组参数;
ii. 但是使用数组的时候,编译器会自动将其转换为指向数组首元素的指针,所以可以把形参写成类似数组的形式(指针)传参;// 这三个函数都是相同的 void print(const int*); void print(const int[]); void print(const int[10]); // 这个10是形参 实参并不一定是10 int i = 0, j[2] = {1, 2}; print(&i); // 正确 传入指向i的指针 print(j); // 正确 传入指向j[0]的指针
iii. 因为数组形参传入的是指向首元素的地址,函数内部其实是不知道数组的大小,所以在遍历这个数组的时候要小心数组越界问题;通常有3种方式解决这个问题(推荐使用前两种方式,第三种局限性很大):
- 使用规范库规范:传递指向数组首元素和尾后元素的指针;
void print(const int *beg, const int *end){ while(beg != end){ cout << *beg++ << endl; } }
- 传入指向数组首元素的指针和该数组size;
// size = end(a) - begin(j); 或者 sizeof(a) / sizeof(a[0]) void print(const int a[], size_t size){ for(size_t i = 0; i != size; ++i){ cout << a[i] << endl; } }
- 标记结尾元素为0(局限性很大);
void print(const int *p){ if(p){ while(*p) cout << *p++ << endl; } }
-
将数组形参定义为数组的引用,引用形参会直接绑定在;
// int (&arr)[10]: 定义了一个引用,引用的对象是一个大小为10的int数组 // int &arr[10]: 定义了一个数组,数组中每个元素都是int型的引用,错误 引用不是对象 不能称为数组中的元素 void print(int (&arr)[10]){ for(auto elem : arr) cout << elem << endl; } int a[10] = {0}; int b[2] = {0, 1}; int c = 0; print(a); // 对 // 注意引用即别名,实参必须和形参类型一致,所以实参必须也是大小为10的int数组 print(b); // 错 print(c); // 错
-
形参是多维数组:
// int (*matrix)[10]:定义了一个指针,指向一个大小为10的int数组 // int *matrix[10]: 定义了一个大小为10的数组,每个元素都是int型的指针 void print(int (*matrix)[10], int row_size){} // 第一个维度不用定义 写了也没用 第二个维度写死了就是10列 void print(int matrix[][10], int row_size){}
5. main函数:处理命令行选项
-
带参数main函数一般形式:
int main(int argc, char *argv[]){} int main(int argc, char **argv[]){} // 推荐用这个
argc表示输入参数的个数,argv为一个数组,存放的是从m命令行输入的所有char字符串; 例子:
int main(int argc, char **argv){ string s = ""; for(int i = 1; i != argc; ++i){ s += string(argv[i]) + " "; } cout << s << endl; return 0; } // 命令行输入: hello world // 命令行输出: hello world
6. 含有可变形参的函数
-
如果形参类型全部相同:使用标准库类型initializer_list 示例:
void error_msg(initializer_list<string> il){ for(auto beg = begin(il) ; beg != end(il); ++beg) cout << *beg << " "; cout << endl; } int main(int argc, char **argv){ // lst可以是任意个数string类型 initializer_list<string> lst = {"error", "hello"}; error_msg(lst); return 0; }
-
实参类型不同,可以使用可变参数模板(后续介绍)
-
省略形参符: …,便于C++访问某些C代码,这些C代码使用了 varargs的C标准功能;
三、返回类型和return语句
-
return语句有两种形式
return; // 只能用在返回类型是void的函数 return expression;
-
无返回值函数:声明void,无需返回值,但是如果需要跳过函数某些语句直接退出函数,可以直接return;
-
有返回值函数
i. 返回值类型必须和函数的返回类型相同,或者能隐式转换成函数的返回类型;
ii. 值是怎么返回的:如果是返回值类型如string,则返回的是一个变量的副本或者一个未命名的临时变量;如果返回值类型是引用类型&,则返回的是返回的对象的一个别名;string get_return(string a){ return a; // 返回a变量的副本 return "hello"; // 返回未命名的临时变量 } string& get_return(string a){ return a; // 返回a变量的一个别名 }
iii. 不要返回局部变量对象的引用或者指针:函数的局部对象在函数执行结束后会被销毁,返回一个已经消耗的对象的引用或指针都是没有意义的;
string& get_return(string a){ string b = "world"; return a; // 对 return b; // 不报错 但是执行的时候报错 return "hello"; // 直接报错 }
-
调用运算符()和点运算符优先级相同,遵循左结合律,所以如果返回类类型可以在后面直接接点运算符调用类对象的属性;
auto sz = shorterString(s1, s2).size();
-
返回类型是非常量引用类型如char&,则这个返回值是一个左值,可以直接为它赋值;
char& get_val(string &str, string::size_type idx){ return str[idx]; } int main(){ string s = "hello world"; get_val(s, 0) = 'H'; cout << s << endl; // Hello world return 0; }
-
c++11规定,函数可以返回花括号{}的值列表,此时返回的是未命名的临时变量;
vector<string> process(){ return {"hello", "world", "h", "k"}; }
-
主函数main的返回值可以没有return,系统会隐式的插入一条返回0的return语句;
-
返回数组指针:数组不能被拷贝,所以不能直接返回数组,但是可以返回数组的指针或者引用;四种方法,尾置法最好,最简单,可读性也最好;
i. 返回数组的指针/引用看起来比较繁使,可以使用类型别名:typedef int arrT[10]; using arrT = int[10]; arrT* func(int i); // 返回一个指针 指向int[10] arrT& func(int i); // 返回一个引用 它是一个int[10]数组的别名
ii. 但是如果想不使用类型别名,数组的维度就必须定义再数组名之后(读起来从内向外,从右往左):
Type (*func(param list))[dim]; int (*func(int i))[10]; // 返回一个指针 指向int[10]; int (&fun())[10]; // 返回一个引用 它是一个int[10]数组的别名
-
c++11允许使用尾置返回类型:
auto func(int i) -> int(*)[10]; // 返回一个指针 指向int[10]; auto func(int i) -> int(&)[10]; // 返回一个引用 它是一个int[10]数组的别名
-
当明确知道返回的指针指向哪个数组时,可以使用decltype关键字声明返回类型:
int odd[] = {1, 2, 3}; decltype(odd) *func(i); // 返回一个指针 指向int[3]; decltype(odd) &func(i); // 返回一个引用 它是一个int[10]数组的别名
四、函数重载
-
函数重载:函数名字相同,但是参数列表不同(和返回值类型无关),一般只重载那些操作相似的函数;
void print(int a); int print(int b); // 错 只有返回值类型不同 不是函数重载 void print(string b); // 对 void print(const string b); // 错 顶层const不算重载
-
main函数不能重载;
-
实参给形参初始化本质上也是变量赋值,所以如果const形参是顶层的,可以忽略;
void print(string b){cout << "1" << endl;} void print(const string b){cout << "2" << endl;} // 错 顶层const不算重载 void print(string *b){cout << "3" << endl;} void print(string* const b){cout << "4" << endl;} // 错 顶层const不算重载 void print(const string* b){cout << "5" << endl;} // 对 底层const 算重载 void print(string &b){cout << "6" << endl;} void print(const string &b){cout << "7" << endl;} // 底层const 算重载
-
当函数的作用相同时,不需要再重写函数,只需使用const_cast调用已经存在的重载函数即可:
const string& shorterString(const string& s1, const string& s2){ cout << "1" << endl; return s1.size() <= s2.size() ? s1 : s2; } string& shorterString(string& s1, string& s2){ cout << "2" << endl; auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); return const_cast<string&>(r); } int main(){ string s1 = "Hello"; string s2 = "jr"; cout << shorterString(s1, s2) << endl; return 0; }
-
函数重载与作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名;
string read(); void print(const string&); void print(double); int main(){ bool read = false; string s = read(); // 错 内层同名 外层的read函数就隐藏了 void print(int); // 1 print("Hello"); // 错 内层同名 外层的read函数就隐藏了 print(3); // 对 调用1 print(3.14); // 对 隐式转换 调用1 return 0; }
五、三个函数相关的语言特性
1. 默认实参
-
一旦某些形参被赋予了默认值,那么它后面的所有形参都必须要有默认值;
string screen(sz h = 24, sz w = 80, char backgrnd = ' '); string screen(sz h = 24, sz w = 80, char backbrnd); // 错 string screen(sz h, sz w = 80, char backgrnd = ' ') // 对
-
默认实参只能省略尾部的实参;
string window; window = screen(); // screen(24, 80, ' ') window = screen(66); // screen(66, 80, ' ') window = screen(66, 256); // screen(66, 256, ' ') window = screen(66, 256, '#'); // screen(66, 256, '#') window = screen(, , '?); // 错 window = screen('?'); // screen('?', 256, ' ');
-
可以多次声明同一个函数;
string screen(sz, sz, char = ' '); string screen(sz, sz, char = '?'); // 错 重复声明 string screen(sz = 24, sz = 80, char);
2. 内联函数和constexpr函数
-
内联函数inline可以避免函数调用的开销,可以让编译器在调用点内联地展开该函数;
-
内联函数适合优化函数规模小但需要频繁调用的函数;
inline const string& shorterString(const string &s1, const string &s2){ return s1.size() <= s2.size() ? s1 : s2; } cout << shorterString(s1, s2) << endl; // 等价于调用时直接在调用点展开该函数 cout << (s1.size() <= s2.size() ? s1 : s2) << endl;
-
内联函数一般都在头文件中定义;
-
“常量表达式函数”指能作用于常量表达式的函数;
-
一个常量表达式必须满足下面两个条件:
i. 整个函数体中除了using、typedef、static_assert以外,有且只有一条return语句;
ii. 函数的返回值和所有形参都必须是字面值类型;constexpr int new_sz(){return 42;}; // 常量表达式函数 constexpr int foo = new_sz(); // foo是常量表达式
-
constexpr函数会被隐式的指定为内联函数;
-
constexpr函数返回类型并不一定为常量表达式:
constexpr int display(int x){ return x; } int main(){ int x = 1; int a[display(x)]; // 错误 此时函数返回的不是常量表达式 int a[display(1)]; // 对 此时函数返回的是常量表达式 return 0; }
3. 调试帮助
-
assert预处理宏:assert(exp);打开调试状态,通常用来检查不能发生的条件;
i. 如果exp表达式为假,assert输出信息并终止程序; 反之assert什么也不做;
ii. #include < cassert>// 假设要求输入的word长度必须要大于阈值 那么就可以用这句话来判断 assert(word.size() > threshold);
-
NDEBUG预处理变量:通常和assert联用,关闭调试状态,跳过assert语句,放在#include < cassert>前面;
#define NDEBUG #include <cassert> using namespace std; int main(){ int x = 0; assert(x); // 关闭NDEBUG 输出 expression: x 打开 跳过这句话 return 0; }
-
NDEBUG除了和assert联用外,还可以单独使用编写调试代码;
_ func _ :输出当前函数名称
_ FILE _ :输出当前文件名称
_ LINE _ :输出当前行号
_ DATE _ :输出文件编译日期
_ TIME _ :输出文件编译时间void print(){ #ifndef NDEBUG cerr << __func__ << "..." << endl; #endif }
六、重载函数匹配
-
重载函数匹配三个步骤:1、寻找候选函数;2、寻找可行函数;3、寻找最佳匹配
候选函数:所有重载函数集;
可行函数:可以被当前这组实参调用的函数(可以隐式转换);
最佳匹配:形参和实参类型越接近,匹配的越好;void f(); // 1 void f(int); // 2 void f(int, int); // 3 void f(double, double = 3.14); // 4 f(5.6); // 候选:1234 可行:24 最佳:4 f(42, 2.56); // 候选:1234 可行:34 最佳:没有最佳 34匹配度一样 报错
-
匹配分级,可以分为5个等级:
i. 精准匹配;
实参和形参类型完全相同;
实参从数组类型或函数类型转换为对应的指针类型;
向实参添加顶层const或者从实参删除顶层const;
ii. 通过const转换实现的匹配;
iii. 通过类型提升实现的匹配(bool、char、short、int、long);
iv. 通过算术类型转换或指针转换实现的匹配;
v. 通过类类型转换实现的匹配;void ff(int); void ff(short); ff('a'); // char->int->3 char->short->4 匹配ff(int) void mainp(long); void mainp(float); mainp(3.14); // double->long double->float都是4 级别一样 错误 Record lookup(Account&); Record lookup(const Account&); const Account a; Account b; lookup(a); // 可行=最佳=lookup(const Account&); lookup(b); // 两个都可行 但是最佳lookup(Account&);
七、函数指针
-
函数指针:依然是一个指针,指向的是一个函数,而非一个对象;
声明函数指针:bool lengthCompare(const string&, const string&); // 定义一个指针pf,指向一个函数 函数返回类型是bool,形参是两个const string引用 bool (*pf)(const string&, const string&); // 定义一个函数 函数名是pf,返回类型是bool*,形参是两个const string引用 bool *pf(const string&, const string&);
使用函数指针:
// 定义一个指针pf,指向一个函数 函数返回类型是bool,形参是两个const string引用 bool (*pf)(const string&, const string&); // pf指向lengthCompare函数 pf = lengthCompare; // 等价 函数名退化为函数指针 pf = &lengthCompare; // 通过函数指针调用该函数 bool flag1 = pf("hello", "goodbye"); // 等价 bool flag2 = (*pf)("hello", "goodbye");
-
指针在指向函数的时候不存在任何的转换规则,返回类型和形参类型必须完全一致;不过可以指向0,即指针不指向任何函数;
bool lengthCompare(const string&, const string&); string::size_type sumLength(const string&, const string&); bool cstringCompare(const char*, const char*); int main(){ // 定义一个指针pf,指向一个函数 函数返回类型是bool,形参是两个const string引用 bool (*pf)(const string&, const string&); pf = 0; // pf不指向任何函数 pf = sumLength; // 返回类型不匹配 pf = cstringCompare; // 形参类型不匹配 pf = lengthCompare; // 对 return 0; }
-
重载函数的指针也一样,在指针指向函数的时候,返回值类型和形参类型必须完全匹配;
void ff(int*); void ff(unsigned int); int main(){ void (*pf1)(unsigned int) = ff; // 对 指向ff(unsigned int)函数 void (*pf2)(int) = ff; // 错 形参不匹配 double (*pf3)(int*) = ff; // 错 返回值类型不匹配 return 0; }
-
函数形参是函数指针类型
i. 函数指针还可以作为另一个函数的形参,形参可以是函数本身(会自动转换为指向函数的指针),也可以是指向函数的指针;bool lengthCompare(const string&, const string&); // 声明 两个等价 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&)); int main(){ // 调用 string s1 = "hello", s2 = "goodbye"; useBigger(s1, s2, lengthCompare); return 0; }
ii. 可以用类型别名typedef和decltype简化代码
typedef bool Func(const string&, const string&); typedef decltype(lengthCompare) Func2; // Func = Func2 都是函数类型 typedef vool (*FuncP)(const string&, const string&); typedef decltype(lengthCompare) *Func2P; // FuncP = Func2P 都是函数指针类型 void useBigger(const string&, const string&, Func); void useBigger(const string&, const string&, FuncP); // 等价 形参可以是函数类型也可以是函数指针类型
-
返回值是函数指针类型
必须返回指向函数的指针,不能返回函数本身using F = int(int*, int); using PF = int(*)(int*, int); PF f1(int); // 正确 可以返回函数指针类型 F f1(int); // 错误 不能直接返回函数类型 F *f1(int); // 正确 返回函数指针类型
可以使用尾置返回类型
auto f1(int) -> int(*)(int*, int);
可以使用类型声明decltype指明返回类型
string::size_type sumLength(const string&, const string&); // 定义genFcn函数的返回类型是指向sumLength的指针类型 decltype(sunLength) *genFcn(const string*);