C++ Prime 第六章 函数
2023-03-18~2023-03-28
函数基础
- 传入的实参需要与形参数据类型相同,或者传入的实参可以隐式转换成形参的数据类型
- 无需定义形参时可以在形参列表可以为空,如果需要兼容C语言形参为空时需要填入void关键字,如:int Test(void){return 1};
- 函数的类型不能是函数或者数组,但可以是指向函数的指针或者指向数组的指针
6.1节练习
- 练习6.1:
实参:调用函数时实际闯入的变量
形参:定义函数时,用来接收传入实参的变量,实参是形参的初始值 - 练习6.2:
(a):函数实际上返回的变量和函数定义的返回类型不一致,将函数的返回类型改成string、或者将返回值的类型改为int
(b):定义缺少返回类型,不返回数据时使用void (c):函数体缺失{,补上{ (d):函数体缺失,把函数体的内容放入{}中 - 练习6.3
using namespace std;
#include<iostream>
int fact(int temp)
{
int ret = 1;
while (temp > 0)
{
ret *= temp--;
}
return ret;
}
int main()
{
cout << fact(5) << endl;
system("pause");
return 0;
}
- 练习6.4:
using namespace std;
#include<iostream>
int fact(int temp)
{
int ret = 1;
while (temp > 0)
{
ret *= temp--;
}
return ret;
}
int main()
{
int num;
cout << "输入数字" << endl;
cin >> num;
cout << fact(num) << endl;
system("pause");
return 0;
}
- 练习6.5:
using namespace std;
#include<iostream>
int fact(int temp)
{
if (temp < 0)
{
temp *= -1;
}
return temp;
}
int main()
{
cout << fact(-1) << endl;
system("pause");
return 0;
}
局部对象
- 自动对象:只存在块执行期间的对象(如形参,执行函数创建形参,函数执行结束,形参销毁)
- 局部静态对象:使用static关键字修改局部对象,令局部对象的生命周期延长至整个程序结束时才会销毁
- 局部变量如果没有显示指定初始值,将会自动执行值初始化
6.1.1节练习
- 练习6.6:形参是一种局部变量,生命周期都伴随函数执行时创建,函数执行结束时局部对象被销毁。局部静态变量程序第一次执行时才会进行初始化操作(以后都不会进行初始化),知道主程序结束时才会进行被销毁
using namespace std;
#include<iostream>
void Test(int num)
{
static int static_num = 0;
cout << "static:" << static_num << endl;
cout << "temp:" << num << endl;
++static_num;
++num;
}
int main()
{
int ret = 10;
while (ret > 0)
{
Test(0);
--ret;
}
system("pause");
return 0;
}
- 练习6.7:
using namespace std;
#include<iostream>
int Test()
{
static int num = 0;
return num++;
}
int main()
{
int num = 10;
while (num > 0)
{
cout << Test() << endl;;
--num;
}
system("pause");
return 0;
}
6.1.2节练习
- 练习6.8:
#pragma once
int fact(int temp);
6.1.3节练习
略...
6.2.1节练习
- 练习6.10:
#include<iostream>
using namespace std;
void swap_int(int* p1, int* p2)
{
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 10;
int b = 20;
swap_int(&a, &b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
system("pause");
return 0;
}
6.2.2节练习
- 练习6.11:
#include<iostream>
using namespace std;
void func(int& p)
{
p = 0;
}
int main()
{
int a = 10;
int& r = a;
func(r);
cout << r << endl;
system("pause");
return 0;
}
- 练习6.12:引用可读性强,并且以接收引用方式接收实参时,不论是值传递还是引用传递都会被转化成引用传递
#include<iostream>
using namespace std;
void swap(int& num1, int& num2)
{
int temp;
temp = num1;
num1 = num2;
num2 = temp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
cout << "a=" << a << endl;
cout << "b=" << b << endl;
system("pause");
return 0;
}
- 练习6.13:一个是值传递、一个是引用传递
- 练习6.14:需要传入的形参的数据类型较大时可以考虑引用类型或者指针,当需要改变形参指向(地址)时不能使用引用传递
- 练习6.15:函数的功能没有需要要改变s的内容,所以s用const修饰。(win11系统64位编译器环境下测试)c是char类型占1字节,s是string类型占用40个字节,引用类型占用4个字节,所以从节约内存空间的角度来说,s使用引用传递,c可以直接使用值传递。occurs需要根据c的出现次数递增不能const修饰
const形参与实参
- 不需要对传入实参的值进行修改时,对形参加上const限定符
6.2.3节练习
- 练习6.16: bool os_empty(const string&s){return s.empty();}
- 练习6.17:不相同,一个是值传递一个是引用
#include<iostream>
using namespace std;
void judgeCap(const string &str1)
{
string str2(str1.size(), ' ');
int num = 0;
for (auto temp : str1)
{
str2[num] = toupper(temp);
++num;
}
for (decltype(str1.size()) i = 0; i < str1.size(); i++)
{
if (str1[i] == str2[i])
{
cout << "含有大写" << endl;
break;
}
}
}
void convLow(string &str)
{
for (auto& temp : str)
{
temp = tolower(temp);
}
}
int main()
{
string str1 = "abcDefGHijk";
judgeCap(str1);
string str2 = "ABCDefGHijk";
convLow(str2);
cout << str2 << endl;
system("pause");
return 0;
}
- 练习6.18:
(a)推测比较大小
bool compare(const matrix &m1, const matrix &m2);
(b)推测修改vector对象的值
vector<int>::iterator& change_val(int num,vector<int>::iterator& r);
- 练习6.19:
(a)不合法,只允许传入一个实参,并且该实参是占位符,并未使用
(b)合法
(c)合法
(d)合法 - 练习6.20:不需要修改传入对象时使用常量引用,可能会导致在不需要修改传入对象指向的值的地方修改了传入对象指向的值
数组形参
- c++不允许对数组进行拷贝操作,所以无法使用值传递的方式对数组进行传递(值传递是一种拷贝),只能使用指针或者引用来传递数组
- 定义数组的引用、指针时需要从内向外理解定义语句(一般情况下定义语句都是从右向左理解)详细原书P102~103
int *p[10] // 定义一个数组p,存放了10个int类型的指针
int (&p)[10] // 定义了一个引用p,引用p绑定在一个存放了10个int类型数据的数组
int (*p)[10] // 定义了一个指针p,指针p指向了一个存放了10个int类型数据的数组
6.2.4节练习
- 练习6.21:const int*:函数没有需要修改传入对象时应该使用const限定符防止在误导、或误操作修改传入对象指向的值
#include<iostream>
using namespace std;
const int& compare(int num, const int* p)
{
if (num > *p)
{
return num;
}
else
{
return *p;
}
}
int main()
{
int a = 20;
int* p = &a;
int ret = compare(3.18, p);
cout << ret << endl;
system("pause");
return 0;
}
- 练习6.22:
#include<iostream>
#include<vector>
using namespace std;
void swap(int* (*p1), int* (*p2))
{
int** temp = p1;
*p1 = *p2;
*p2 = *temp;
}
int main()
{
int a = 10;
int b = 20;
int* p1 = &a;
int* p2 = &b;
cout << "&p1=" << p1 << endl;
cout << "&p2=" << p2 << endl;
cout << "p1=" << *p1 << endl;
cout << "p2=" << *p2 << endl;
swap(p1, p2);
cout << "&p1=" << p1 << endl;
cout << "&p2=" << p2 << endl;
cout << "p1=" << *p1 << endl;
cout << "p2=" << *p2 << endl;
system("pause");
return 0;
}
- 练习6.23: 略...
- 练习6.24:没有传入数组对象时,没有传入数组的长度,当传入的实参长度小于10时会引发数组越界。解决方法:可以将形参修改为固定长度为10的数组的引用,这样就对实参进行了限制,传入长度不等于10的数组的时会及时报错
#include<iostream>
using namespace std;
void print(const int (&arr)[10])
{
for (auto elem : arr)
{
cout << elem << endl;
}
}
int main()
{
system("pause");
return 0;
}
6.2.5节练习
使用visual studio添加命令行参数
- 练习6.25、练习6.26::
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
string str = "";
for (int i = 0; i < argc; ++i)
{
str = str + argv[i] + " ";
}
cout << str << endl;
system("pause");
return 0;
}
函数的可变参数
- 类型相同但数量未知
- initializer_list中的数据都是const类型
- 对initializer_list类型的数据进行拷贝或者赋值都是浅拷贝(如下,list1和list2中指向的元素地址是相同的,不同的是list1和list2本身的地址)
initializer_list<string> list1({ "qwer", "asdf", "zxcv" });
initializer_list<string> list2 = list1;
形参例子:
#include<iostream>
using namespace std;
void error_msg(initializer_list<string> li)
{
for (auto beg = li.begin(); beg < li.end(); ++beg)
{
cout << *beg << endl;
}
}
int main()
{
error_msg({ "qwer", "asdf", "123" });
error_msg({ "qwer", "123" });
system("pause");
return 0;
}
6.2.6节练习
- 练习6.27:
#include<iostream>
using namespace std;
void sum_nums(initializer_list<int> nums)
{
int temp = 0;
for (auto beg = nums.begin(); beg < nums.end(); ++beg)
{
temp += *beg;
}
cout << temp << endl;
}
int main()
{
sum_nums({ 1, 2, 3, 4, 5 });
system("pause");
return 0;
}
- 练习6.28:const string&
- 练习6.29:不需要,initializer_list中的数据都是const,无法修改所以不需要引用
返回类型和return语句
- 没有返回值的return语句只能使用在返回类型是void的函数中,返回类型是void的函数中如果不写retrun编译器会在函数的最后一行隐式的执行return
- 特殊情况:main语句可以不写reutrn,编译器会自动在最后一行隐式的加上return 0;// 0代表成功
- 不要返回局部对象的引用或指针,因为局部对象在函数调用结束后就被销毁了,返回他们的引用或者指针会导致指向不确定的值
- 函数调用的结果可以用来访问对象成员
- 返回一个值的方式和初始化一个变量或形参的方式完全一样,返回的值被作为调用点的一个临时量,该临时量就是函数调用的结果。
auto sz = shorterString(s1, s2).size();// shortterSting函数返回的是一个const &string类型的引用
- 引用返回左值:如果函数的返回类型是引用那么返回的就是左值,其他类型返回的都是右值。因为是左值所以可以被赋值
get_val(s, 0) = 'a';//get_val返回的是一个char&类型的引用
- 递归:main函数不能进行递归操作
6.3.2节练习
- 练习6.30:提示缺少必要返回值
- 练习6.31:返回局部对象的引用(无效)
- 练习6.32:依次给int类型的数组赋值
- 练习6.33:
#include<iostream>
#include<vector>
using namespace std;
void print_vec(const vector<int> &vec, vector<int>::iterator iter)
{
if (iter < vec.end())
{
cout << *iter << endl;
++iter;
print_vec(vec, iter);
}
}
int main()
{
vector<int> vec = { 1, 2, 3, 4, 5, 6 };
vector<int>::iterator iter = vec.begin();
print_vec(vec, iter);
system("pause");
return 0;
}
- 练习6.34:后置递增、递减作为递归出口会导致死循环
返回数组指针的函数声明
// 伪代码
Type(*func(pram))[dimension] ;// 例子:
int i = 0;
int (*func(i))[10]
- 使用尾置返回:
int i = 0;
int func(i) -> int(*)[10]
- 使用decltype
#include<iostream>
#include<vector>
using namespace std;
int odd[]{ 1, 3, 5, 7, 9 };
int even[] = { 0, 2, 4, 6, 8 };
decltype(odd)* arrPtr(int i) { return (i % 2) ? &odd : &even; };
int main()
{
cout << *arrPtr(1)[0] << endl;;
system("pause");
return 0;
}
6.3.3节练习
- 练习6.36:
#include<iostream>
#include<vector>
using namespace std;
string(&(func(string (&str)[10])))[10]
{
return str;
}
int main()
{
string str1[10] = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" };
string (&str2)[10] = func(str1);
for (auto temp : str2)
{
cout << temp << endl;
}
system("pause");
return 0;
}
- 练习6.37:使用尾置返回类型和类型别名结构比较清晰、decltype需要先定义数组才能使用
#include<iostream>
#include<vector>
using namespace std;
// 使用类型别名
typedef string arr[10];
arr& func1(arr& str)
{
return str;
}
// 使用尾置返回类型
auto func2(arr& str) -> string(&)[10]
{
return str;
}
// 使用decltype
string str1[10] = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" };
decltype(str1)& func3(arr& str)
{
return str;
}
int main()
{
string (&str2)[10] = func3(str1);
for (auto temp : str2)
{
cout << temp << endl;
}
system("pause");
return 0;
}
- 练习6.38:查看练习6.37中的decltype
函数重载
- 根据形参数量、类型来调用相匹配的函数
- 不能通过顶层const来进行函数重载, 例子:
void lookup(Phone);
void lookup(const Phone);
void lookup(*Phone);
void lookup(const *Phone)
6.4节练习
(a)错误,不能通过顶层const来进行函数重载
(b)错误,不能通过返回类型进行函数重载
(c)通过形参是否是int、double类型的指针进行函数重载
重载与作用域
- 名字查找发生在类型检查之前,所以多层作用域都存在重载函数时,会现在内层作用域内查找函数,查找到就不会在外层作用域查找函数,之后在进行形参数据类型的检查。(相当于内层作用域进行函数重载时会屏蔽外层作用域的函数重载)
默认实参
- 一旦形参被赋予的默认值,这个形参后面的所有形参都必须有默认值
- 在给定的作用域内,一个默认形参只能被赋予一次
void func(int a, string b, char c = 'c'); // 有效
void func(int a, string b, char c = 'c');; // 错误,一个默认形参在给定作用域只能被赋予一次
void func(int a, string b = "bb", char c); // 正确,为没有默认值的形参添加默认参数
void func(int a = 10, string b, char c); //正确
- 局部变量不能作为默认实参
练习6.5.1
- 练习6.40: (b)错误,从第一个默认形参开始,后续的所有的形参必须有默认值
- 练习6.41: (a)非法,缺少形参ht对应的实参,(b)、(c)虽然合法但是背离了程序设计的初衷,调用函数只需要传入形参ht对于的实参就行
- 练习6.42: 这题目应该是要给ending这个形参赋予默认形参s
#include<iostream>
using namespace std;
string make_plural(size_t ctr, const string& word, const string& ending = "s")
{
return (ctr > 1) ? word + ending : word;
}
int main()
{
cout << make_plural(1, "success") << endl;
cout << make_plural(2, "success") << endl;
cout << make_plural(1, "failure") << endl;
cout << make_plural(2, "failure") << endl;
system("pause");
return 0;
}
内联函数和consterpr函数
- 函数调用比一般求等价表达式慢
- 函数调用的工作流程:调用前保存寄存器,并在返回时回复。可能需要拷贝实参,程序转向一个新的位置继续执行。
- 内联函数可以避免函数调用的开销,内联函数会将函数在每个调用点的位置将函数“以内联的方式”展开。如下:
cout << shortString(s1, s2) << endl; // 假设shortString是内联函数,实际上编译在编译过程中将函数内联展开成如下面的形式
cout << (s1.size() < s2.size() ? s1 : s2) << endl; // 这样就消除了函数调用的开销
- 在函数的返回类型前加上inline关键字,就可以将函数声明成内联函数,但编译时编译器会不会将函数转化成内联函数是不确定,一般代码规模小、流程直接、频繁调用的函数会被转化成内联函数。
inline const string &shortString(const string &s1, const string &s2){ return s1.size() <= s2.size() ? s1 : s2};
- constexpr函数:能用于常量表达式的函数,需要遵循以下几点:函数的返回类型、所有形参必须是字面值,而且函数体中只能有一条return语句。
- 调用constexpr函数时,编译器会把函数的调用替换成结果值,相当于将函数隐式的转化为内联函数
- constexpr函数的返回值也可能不是一个常量表达式,如下:
#include<iostream>
using namespace std;
constexpr int new_sz(int i)
{
return i * 3;
}
int main()
{
int a = 10;
const int b = new_sz(a);
system("pause");
return 0;
}
因为new_sz()接收了一个非常量的变量作为参数并参与的返回值的计算,所以new_sz()函数无法在编译阶段就知道变量的值,返回的就不是一个常量表达式,因此无法在编译阶段就知道这个函数的返回值,所以不会隐式的转化成内联函数
内联函数和constexpr函数在头文件内定义
- 编译器想在编译阶段展开函数仅靠函数的声明是不够的,并且内联函数和constexpr函数允许在不同的翻译单元(不同的cpp文件)内重复定义并一致,基于这个原因内联函数、constexpr函数通常定义在文件中
6.5.2节练习
- 练习6.43:(a)放到头文件中,(b)放到源文件中
- 练习6.44:
inline bool isShorter(const string &s1, const string &s2)
{ return s1.size() < s2.size()};
- 练习6.45:略
- 练习6.46:不行,constexpr函数的形参和返回值必须是字面值
调试帮助
- assert(expr)预处理宏,如果expr为假输出信息并终止程序,如果为真不做任何操作
6.5.3节练习
- 练习6.47:
#include<iostream>
#include<vector>
using namespace std;
void printVector(vector<int> &myVector, vector<int>::iterator iter)
{
if (myVector.end() > iter)
{
cout << *iter << endl;
//#define NDEBUG = 1
#ifndef NDEBUG
cerr << __func__ << endl;
cerr << __FILE__ << endl;
cerr << __LINE__ << endl;
cerr << __TIME__ << endl;
cerr << __DATE__ << endl;
#endif // !NDEBUG
printVector(myVector, ++iter);
}
}
int main()
{
vector<int> temp = { 1, 2, 3, 4, 5 };
printVector(temp, temp.begin());
system("pause");
return 0;
}
- 练习6.48:不合理
函数匹配
- 候选函数:函数定义的函数名与被调用的函数名相同并且,函数的定义在函数调用的作用域内可见
- 可行函数:函数的形参与实参数量相同,函数的形参类型与实参相同,或者实参可以转化成与形参类型相同的实参
6.6节练习
- 练习6.49:如上诉
- 练习6.50:
(a):不合法,可行函数:f(int, int)、f(double, double=3.14) 最佳匹配:无最佳匹配,导致二义性
(b):可行函数:f(int)、f(double, double=3.14) 最佳匹配:f(int)
(c):可行函数:f(int, int)、f(double, double=3.14) 最佳匹配:f(int, int)
(d):可行函数:f(int, int)、f(double, double=3.14) 最佳匹配:f(double, double=3.14)
6.6.1节练习
- 练习6.52:
(a)类型提升->小整型提升到int类型
(b)算数类型转化,double->int - 练习6.53 (a)、(b)通过底层const来重载函数 (c)不合法,不能通过顶层const来重载函数
函数指针
- 用指针指向函数,例子
bool (*pf)(const string&, const string&);// pf是一个指针,指向了一个函数
bool* pf(const string&, const string&);// pf是一个函数,返回值是bool*类型的指针
- 使用函数名作为右值时,该函数会自动转化成指针,并且可以通过指针直接调用函数不需要解引用(解引用后也可以正常调用),如下:
pf = lengthCompare;
pf = &lengthCompare; //两个语句等价(lengthCompare是一个函数)
pf();
- 将函数指针赋值为零或者赋值为nullptr代表该指针没有指向任何一个函数,但是不能用该指针指向不同函数类型的函数,函数类型由函数的返回值类型、形参类型决定
- 因为函数指针不能指向不同类型的函数,所以当函数指针指向重载函数时,指针类型必须与重载函数中的某一项精确匹配,如下:
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; //pf1指针指向了ff(unsigned int)函数
void (*pf2)(int) == ff; // 错误:返回值匹配但形参类型不匹配
double (*pf3)(int*) == ff; // 错误:形参列表匹配但返回值不匹配
函数指针作为形参
- 例子:
#include<iostream>
using namespace std;
bool lengthCompare(const string& s1, const string& s2)
{
return s1.size() > s2.size();
}
// 方法1
void useBigger1(const string& s1, const string& s2, bool pf(const string& s1, const string& s2))
{
cout << pf(s1, s2) << endl;;
}
// 方法2
void useBigger2(const string& s1, const string& s2, bool (*pf)(const string& s1, const string& s2))
{
cout << pf(s1, s2) << endl;;
}
int main()
{
string s1 = "asd";
string s2 = "qwe";
useBigger2(s1, s2, lengthCompare);
system("pause");
return 0;
}
- 借助类型别名简化函数指针作为形参
#include<iostream>
using namespace std;
bool lengthCompare(const string& s1, const string& s2)
{
return s1.size() > s2.size();
}
typedef bool Func(const string& s1, const string& s2);
void useBigger(const string& s1, const string& s2, Func* func)
{
cout << func(s1, s2) << endl;
}
int main()
{
string s1 = "asd";
string s2 = "qwe";
useBigger(s1, s2, lengthCompare);
system("pause");
return 0;
}
+使用decltype
#include<iostream>
using namespace std;
bool lengthCompare(const string& s1, const string& s2)
{
return s1.size() > s2.size();
}
typedef decltype(lengthCompare)* Func;
void useBigger(const string& s1, const string& s2, Func func)
{
cout << func(s1, s2) << endl;
}
int main()
{
string s1 = "asd";
string s2 = "qwe";
useBigger(s1, s2, lengthCompare);
system("pause");
return 0;
}
6.7节练习
- 练习6.54:
#include<iostream>
#include<vector>
using namespace std;
int reviceInt(int, int) { return 1; };
typedef decltype(reviceInt)* RIP;
int main()
{
RIP ptr1 = reviceInt;
RIP ptr2 = reviceInt;
vector<RIP> temp = { ptr1 , ptr2 };
cout << temp[0](1, 1) << endl;
cout << temp[1](1, 1) << endl;;
system("pause");
return 0;
}
- 练习6.55:
#include<iostream>
#include<vector>
using namespace std;
int myAdd(int num1, int num2) {
return num1 + num2;
}
int myDelete(int num1, int num2) {
return num1 - num2;
}
int myMul(int num1, int num2) {
return num1 * num2;
}
int myDiv(int num1, int num2) {
return num1 / num2;
}
typedef decltype(myAdd)* Result;
int main()
{
Result ptr1 = myAdd;
Result ptr2 = myDelete;
Result ptr3 = myMul;
Result ptr4 = myDiv;
vector<Result> temp = { ptr1 , ptr2, ptr3, ptr4 };
cout << temp[0](2, 2) << endl;
cout << temp[1](2, 2) << endl;
cout << temp[2](2, 2) << endl;
cout << temp[3](2, 2) << endl;
system("pause");
return 0;
}
- 练习6.56:见练习6.55
C++Prime学习笔记 文章被收录于专栏
勇敢和愚蠢只有一剑之差