c++primer第五版第六章重点笔记

函数

函数:命名的代码块,通过调用函数执行相应代码。由0或多个参数,可重载函数名相同,但形参类型或数量必须有所区别)。

1、 函数基础

(1)函数定义包括:返回类型函数名、由0或多个形参组成的列表函数体

(2)组成函数的形参列表可以为空,但是不能省略;有两种方法定义不带形参的函数书写一个空的形参列表,使用关键字void表示;
  形参列表中的形参有逗号隔开,每个形参都必须含有一个声明符的声明
  任意两个形参都不能同名(一般形参都有名字,便于使用),同时函数最外层作用域中的局部变量名也不能和形参同名。

(3)返回类型:函数通常使用return语句结束函数的执行过程,同时返回与函数返回类型相同的值;但当函数不需要返回值时,通常将返回类型定义为void
  函数不能返回数组类型和函数类型,但是可以返回指向数组或函数的指针

void fi(){/*....*/} //隐式定义空形参列表
void f2(void){/*....*/} //显式定义空形参列表
int f3(int a,int b){/*....*/}

(4)C++中,名字有作用域:名字的作用域是程序文本的一部分,名字在其中可见;对象有生命周期:生命周期是程序执行过程中对象存在的一段时间。
  函数体作为一个语句块,构成一个新的作用域,形参和函数体内定义的变量统称为局部变量;局部变量仅在函数作用域内可见,同时局部变量会隐藏在外层作用域的其他声明中
  只存在于执行期间的对象称为自动对象,作为普通局部变量的形参就是一种自动对象。自动对象变量本身不含初始值,则执行默认初始化,即内置类型额未初始化局部变量会产生未定义的值。
  当需要将局部变量的生命周期贯穿函数电泳及之后的时间时,可以将其定义为static类型,得到局部静态对象。局部静态对象在程序的执行路径第一次经过对象时被初始化,之后经过不会再执行初始化操作,直到程序终止才被销毁
  如果局部静态对象没有显示初始化则执行值初始化

(5)函数在使用之前需要对其名字进行声明函数声明(函数原型)不需要函数体也无需形参名字(写上名字便于理解),用分号代替函数体,需要形参类型
  函数尽量在头文件中声明在源文件定义,这样若想改变函数接口,则只需要改变一条声明。

(6)当程序越来越复杂时,C++支持分离式编译:即将程序分割至多个文件中,独立编译。

2、 函数调用

(1)通过调用运算符()执行函数,作用于一个函数指向函数的指针的表达式,调用运算符()内是用逗号隔开的实参,实参是形参的初始值,且实参与形参存在对应关系:第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推;实参类型必须与对应的形参类型匹配或者可以转换

(2)当调用函数时,主调函数中断,被调函数执行;步骤:首先使用实参初始化形参,然后将控制权转交给被调用函数。

函数调用过程中进行参数传递,使用实参对形参进行初始化,形参初始化方式与变量初始化一样;形参的类型决定了形参与实参交互的方式
(3)形参与实参是相互独立的对象,实参的值被拷贝给形参时,此时称实参为被值传递,或函数被传值调用
  传值参数:函数对形参做的所有操作都不会影响实参。
  指针形参:指针作为形参,函数接受一个指针,通过指针可以改变其所指对象的值,但是作为实参的指针本身并未被改变。(C++中建议使用引用类型的形参代替指针)

#include <iostream>
using namespace std;
void trans(int *a, int *b);
int main() {
	int i = 32,j = 67;
	trans(&i,&j);
	cout << "i = " << i << ",j = " << j << endl;
	system("pause");
	return 0;
}
void trans(int *a,int *b){
	int c = *a;
	*a = *b;
	*b = c;
}

(4)当形参为引用类型时,为传引用参数,此时函数可以通过使用引用形参,改变一个或多个实参的值。
形参使用引用形式的好处:

  1. 可以避免大的类类型对象或者容器对象拷贝时的程序低效表现,甚至某些类类型(IO类型等)不支持拷贝(函数无须修改引用形参值时最好使用常量引用);
  2. 一般函数只能返回一个值,当需要返回多个值时使用引用形参,通过修改引用形参直接改变实参的值,做到返回多个值;还可以定义一个包含多个成员的新的数据类型,但前一种方法明显更简单。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string::size_type find(const string &s, char c, string::size_type &cnnt);
void exchange(int &n, int &m);
int main(){
	//练习6.11,传引用参数
	string s;
	cout << "请输入需要判断的字符串" << endl;
	getline(cin, s);
	string::size_type cnt = 0;
	auto first_f = find(s, 'f', cnt);
	if (first_f == s.size())
		cout << "字符串" << s << "中没有出现字符f" << endl;
	else
		cout << "字符f在字符串" << s << "中首次出现的位置是:" << first_f << ",总共出现了" << cnt << "次" << endl;

	//练习6.12 使用引用形参交换两个整数的值
	int a, b;
	cout << "请输入需要交换的两个整数" << endl;
	cin >> a >> b;
	cout << "交换前a=" << a << ",b=" << b << endl;
	exchange(a, b);
	cout << "交换后a=" << a << ",b=" << b << endl;
	system("pause");
	return 0;
}
string::size_type find(const string &s, char c, string::size_type &cnnt){
	auto ret = s.size();
	for (auto i = 0; i != s.size(); ++i){
		if (s[i] == c){
			if (ret == s.size())
				ret = i;
			++cnnt;
		}
	}
	return ret;
}

void exchange(int &n, int &m){
	int mid = n;
	n = m;
	m = mid;
}

(5)当形参是const时,需注意顶层const和底层const的区别:实参初始化形参会忽略顶层const(顶层const,传入常量对象和非常量对象均可以),所以重载函数时,形参具有顶层const和没有顶层const是相同的。
  形参的初始化方式与变量的初始化方式相同(可以使用非常量初始化一个底层const,反过来不行)。
  最好将函数不需要修改的引用形参定义为常量引用**:①若定义为普通引用,会误导调用者可修改实参的值;②不能将const对象,字面值或需类型转换的对象传递给普通引用;③定义为普通引用的函数不能在正确定义的函数中使用

#include <iostream>
#include <string>
#include <vector>
using namespace std;
void capital(const string &s, string::size_type &cnt);
void capital1(string &s, string::size_type &cnt);
int main(){
		cout << "请输入需要判断的字符串:" << endl;
	string s1; //定义变量存放读取的字符串
	string::size_type cnt1 =0; //定义变量存放字符串中大写字母的个数
	while (getline(cin, s1)){
		capital(s1, cnt1);
		if (cnt1 != 0)
			cout << "本行字符串中含有大写字母个数为:" << cnt1 << "个" << endl;
		else
			cout << "本行字符串中不含有大写字母" << endl;
		cout << "是否继续输入,y/n" << endl;
		string s2;
		cin >> s2;
		if (s2.empty() || s2[0] == 'n')
			break;
		else{
			cnt1 = 0;
			cout << "请输入需要判断的字符串:" << endl;
		}

		cin.get();
	}

	//改为小写形式
	cout << "请输入需要判断的字符串:" << endl;
	string s1; //定义变量存放读取的字符串
	string::size_type cnt1 = 0; //定义变量存放字符串中大写字母的个数
	while (getline(cin, s1)){
		capital1(s1, cnt1);
		if (cnt1 != 0)
			cout << "改写后的字符串为:"<< s1 << ",本行字符串中含有大写字母个数为:" << cnt1 << "个" << endl;
		else
			cout << "本行字符串中不含有大写字母" << endl;
		cout << "是否继续输入,y/n" << endl;
		string s2;
		cin >> s2;
		if (s2.empty() || s2[0] == 'n')
			break;
		else{
			cnt1 = 0;
			cout << "请输入需要判断的字符串:" << endl;
		}
		cin.get();	
	}

	system("pause");
	return 0;
}
void capital(const string &s, string::size_type &cnt){
	for (decltype(s.size()) i = 0; i != s.size(); ++i){
		if (s[i] >= 'A' && s[i] <= 'Z')
			++cnt;
	}
}
void capital1( string &s, string::size_type &cnt){
	for (decltype(s.size()) i = 0; i != s.size(); ++i){
		if (s[i] >= 'A' && s[i] <= 'Z'){
			s[i] = tolower(s[i]);
			++cnt;
		}
	}
}

(6)形参为数组时:因为数组不可拷贝且在转换时通常会被转换为指针,所以无法使用值传递方式,但可以将形参写成类似数组的形式:

void print(const int *);
void print(const int[]);
void print(const int[10]);

上面三个函数等价,形参都是const int*,传递数组给上面函数时,实参自动转换为指向数组首元素的指针。
  指针形参的管理有三种方式:

  1. 使用标记指定数组长度(适合用于有明显结束标记的,如C风格字符串,最后有一个空字符)
void print(const char *cp){
	if(cp)
		while(*cp)
		cout <<*cp++<<endl;
}
  1. 使用标准库规范(传递指向数组首元素和尾后元素的指针
void print0(const int *beg, const int *end){
	while (beg != end)
		cout << *beg++ << endl;
}
  1. 显示传递一个表示数组大小的形参
void print1( const int a[], size_t size){
	for (size_t i = 0; i != size; ++i)
		cout << a[i] << endl;
}

(7) C++允许将变量定义为数组的引用,同样形参也可以是数组的引用(由内向外阅读):
void f (int (&arr) [10] ) //arr是具有10个整数的整形数组的引用
  数组大小作为数组类型的一部分,在传递过程中限制了函数的可用性,只能使用大小确定的数组

(8)传递多维数组给函数时,同样真正传递的是数组首元素的指针(多维数组首元素也是一个数组),第一个维度会被忽略,

void print(int (*maxtix)[10]);
void print(int maxtix[][10]); //maxtix都是指向含有10个整数的数组的指针

(9)有时也需要给main函数传递实参

int main(int argc, char *argv[]{...}

其中argv是一个数组,第一个元素argv[0]指向程序的名字或一个空字符串,从argv[1]才开始传递可选的实参

(10)当无法预知传递的实参数量时,使用含有可变形参的函数:

  1. 实参类型不同时,采用可变参数模版
  2. 实参类型相同时,传递initializer_list标准库类型(用于表示某种特定类型的值的数组),
    initializer_list lst; //初始化
      initializer_list中的元素永远是常量值
      同vector一样可以使用size()、begin()、end()操作,也可使用范围for循环
      含有initializer_list形参的函数也可以含有其他形参
      定义initialize_list对象时,必须说明列表中所含元素的类型
      如果向initializer_list形参传值序列,需要花括号括起来。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int sum(initializer_list<int> li);
int main(int argc, char *argv){
	initializer_list<int> la{ 2, 3, 5, 6, 7 };
	int sum0 = sum(la);
	cout << "la列表中元素之和为:" << sum0 << endl;
	system("pause");
	return 0;
}
int sum(initializer_list<int> li){
	int sum = 0;
	for (auto beg = li.begin(); beg != li.end(); ++beg)
		sum += *beg;
	return sum;
}

(11)省略符作为特殊的形参符号,仅用于C和C++通用类型,只可以出现在形参列表的最后一个位置,不需要类型检查。

3、函数返回

return语句终止正在执行的函数并将控制权返回到调用该函数的地方;有两种形式:return;和return expression
函数函数分为有返回值和无返回值函数:
(1)无返回值函数:其返回类型为void,return语句的return;形式只能用于void函数

  • void函数不要求有return语句,会隐式执行;
  • 可在中间位置使用return;直接结束函数提前退出;
  • 若使用了return expression;形式的return语句,则返回的必须是另一个返回void的函数,否则会出错。

(2)有返回值的函数:只要函数的返回值不是void,则函数内的每条return语句都必须返回一个值值类型也必须与函数的返回类型相同或能隐式转换

  • 含有return语句的循环后也一定要有一个循环语句;
  • 值返回的方式和初始化一个变量或形参的方式完全一样
  • 函数不要返回局部对象的引用或指针,因为函数终止后,其中定义的变量将被销毁,引用或地址将指向无效区域或地址。

(3)调用运算符和点运算符、箭头运算符优先级相同,并且都满足左结合律

auto sz = shorter(s1, s2).size();

(4)函数的返回类型决定函数调用是否是左值

  • 调用返回引用的函数得到左值,可以向其他左值一样出现在赋值运算符的左侧,为其赋值。
  • 调用其他返回类型得到的是右值。

(5)函数可以返回花括号包围的值的列表,可用该列表初始化表示函数返回的临时量,若列表为空,临时量执行值初始化。若返回的是内置类型,列表最多包含一个值;若返回的是类类型,由类本身定义初始值如何使用。

(6)主函数main的返回值:允许main函数没有返回值,编译器会隐式插入返回0的return语句,表示成功;非0值表示的具体含义由机器决定,为了是返回与机器无关,cstdlib头文件定义了两个预处理变量EXIT_FAILURE表示失败,EXIT_SUCCESS表示成功。

(7)如果函数调用了自己本身,则称为递归函数

void vector_ys(vector<string> v1,int ix);
//练习6.33
int main(int argc, char *argv){
	string a[10] = { "jlk", "uu", "pkop", "iyh", "tyr", "utg", "qers", "uib", "est", "iu" };
	vector<string> f(a, a+10);
	vector_ys(f,f.size());
	system("pause");
	return 0;
}

void vector_ys(vector<string> v1,int ix){
	//int i = 0;
	if (ix > 0){
		cout << v1[ix-1] << endl;
		--ix;
		vector_ys(v1, ix);
	}
}

(8)返回数组指针:因为数组不可拷贝,所以函数也不能返回数组,只能返回数组的指针或者引用
一个返回数组的函数的声明如下:
Type (function(parameter_list) ) [dimension]
Type表示元素的类型,dimension表示数组大小, (*function ( parameter_list) )两端括号必须存在,同样由内向外阅读理解该声明语句。
  表示调用function函数时需要parameter_list类型的形参,并可对该函数调用结果执行解引用操作,解引用后的带一个大小为dimension的数组,数组中的元素为Type 类型。

(9)对于上述声明语句有三种简化方式:

  1. 使用类型别名
    typedef Type arrT[dimension];

    using arrT = Type[dimension];
    arrT* function(parameter_list);
  2. 使用尾置返回类型
    auto function(parameter_list) -> Type (*) [dimension];
  3. 使用decltype(需要知道函数返回的指针指向哪个数组)
    decltype(指向的数组) *arrT(parameter_list);

4、函数重载

(1)重载函数:同一作用域内的几个函数名字相同形参列表不同。函数重载可以再一定程度上减轻程序员起名字、记名字的负担。

  • 形参名并不能区分函数
  • 顶层const不影响传入函数的对象(拥有顶层const的形参无法与没有顶层的形参区分);
  • 如果形参是某种类型的指针或者引用,底层const可以区分形参(区分其指向的是常量还是非常量)

(2)不允许两个函数除返回类型不同外其他所有要素都相同

(3)函数形参是常量,但是实参不是常量,并且想返回得到一个非常量的普通引用可以使用const_cast强制转换得到。其转换过程为:实参非常量——>函数1(形参非常量引用)—强制转换—>函数2(实参常量引用)—返回—>常量引用(绑定在非常量)—强制转换—>非常量引用返回值。

(4)调用重载函数有三种可能结果

  • 得到最佳匹配,实参与形参完美匹配;
  • 找不到任何函数与调用的实参匹配,发出无匹配错误;
  • 多于一个函数可以匹配,但不是最佳选择,显示二义性调用

(5)函数声明最好不要置于局部作用域内,因为在内层作用域内声明名字,将会隐藏外层作用域中声明的同名实体,且在不同的作用域中无法重载函数。名字查找发生在类型检查之前。

5、与函数相关的语言特性

默认实参、内联函数和constexpr函数
(1)函数多次调用中,某个形参都被赋予相同的值,将这个反复出现的值称为函数的默认实参

  • 调用含有默认实参的函数时,可以包含该实参也可以省略;
  • 一旦某个实参被赋予了默认值,它后面的所有形参都必须有默认值
  • 若想使用默认实参,在调用函数时忽略该实参就可以了;
  • 函数调用时实参按照位置解析,默认实参负责填补函数调用缺少的尾部实参;
  • 可以多次对函数进行声明,但是一个形参只能赋予一次默认值,并且不能在声明中修改一个已经存在的默认值
  • 只要表达式的类型能转换成形参所需的类型,就能作为默认实参,但局部变量不能作为默认实参
string make_plural(size_t ctr, const string &word , const string &ending = "s");
//练习6.42
int main(int argc, char *argv){
	string s1 = "success";
	string s2 = "failure";
	cout << "输出s1和s2的复数形式: " << make_plural(2, s1,  "es") << " " << make_plural(2, s2) << endl;
	cout << "输出s1和s2的单数形式:" << make_plural(1, s1) << " " << make_plural(1, s2) << endl;
	system("pause");
	return 0;
}
string make_plural(size_t ctr, const string &word, const string &ending ){
	return (ctr > 1) ? word + ending : word;
}

(2)将规模较小的操作定义为函数,阅读理解较为容易、确保行为统一、修改函数更为方便、可重复利用,但是调用函数比表达式求值要慢,因此提出内联函数
内联函数:在函数的返回类型前加关键字inline即可,函数指定为内联函数,即将它在每个调用点内联的展开。
内联机制主要用于优化规模较小,流程直接,频繁调用的函数。

(4)constexpr函数是指能用于常量表达式的函数;

  • 函数的返回类型和形参类型都必须为字面值常量;
  • 函数体中有且只有一条return语句;
  • constexpr函数不一定返回常量表达式。

(5)内联函数和constexpr函数可以在程序中多次定义,但对于给定的内联函数和constexpr函数,多个定义必须完全一致

(6)头文件保护技术:程序包含一些用于调试的代码,但这些代码只在开发程序时使用,程序编写完发布前,需要先屏蔽掉调试代码,该方法用到两项预处理功能:assert和NDEBUG

  • assert预处理宏(预处理变量),选择一个表达式作为条件
    assert(expr);
    表达式为假(0),输出信息并终止程序,为真什么也不做。
    assert定义在cassert头文件中,因为是预处理变量所以不需要声明,它的名字必须唯一
  • NDEBUG预处理变量assert的行为也依赖于NDEBUG的状态,如果定义了NDEBUG,assert什么也不做,默认状态NDEBUG没有定义,assert执行。
    定义方式:#define NDEBUG;
    NDEBUG作为自己调试的开关,若没有定义,则执行#ifndef NDEBUG和#endif间的代码
void printt(const vector<string> & v, int ix)int main(int argc, char* argv){
	vector<string> v1{ "gi", "uh", "pk", "fy", "ytv", "vh", "yhgi", "ygb", "yuhbop", "tfv" };
	//int iix = v1.size();
	//printt(v1, iix);
	printt(v1);
	system("pause");
	return 0;
}
void printt(const vector<string> & v, int ix){
#ifndef NDEBUG
	cout << "vector size" << v.size() << endl;
#endif
	if (ix > 0){
		cout << v[ix-1] << endl;
		--ix;
		printt(v, ix);
	}

6、函数匹配步骤及具体方法

调用重载函数时,函数的形参数量相等以及某些形参类型可以由其他类型转换得到时,函数匹配变得不容易。
(1)函数匹配的步骤

  1. 选定调用对应的重载函数集,集合中函数称为候选函数与被调函数同名其声明在调用点可见);
  2. 从候选函数中选出与调用所提供的实参相对应的函数,称为可行函数(形参数量与本次调用提供的实参数量相等,每个实参类型与对应形参类型相同);【考虑默认实参的情况】
  3. 从可行函数中选择与本次调用最匹配的函数(实参类型与形参类型越接近,匹配越精确),如果多个函数中没有一个脱颖而出(该函数每个实参的匹配都不劣于其他可行函数需要的匹配,至少有一个实参的匹配优于其他可行函数提供的匹配),则报告二义性调用信息

(2)调用重载函数尽量避免强制类型转换,如果需要强制类型转换,说明设计的形参不合理。

(3)实参类型到形参类型转换分为以下几个优先级

  1. 精确匹配包括:
    实参与形参类型相同;
    实参从数组类型或函数类型转换为对应的指针类型;
    实参进行顶层const添加或删除;
  2. 通过const转换实现的匹配(指向非常量的指针到指向相应常量)。
  3. 类型提升实现的匹配。
  4. 算术类型转换实现的匹配。
  5. 通过类类型转换实现的匹配。
    (内置类型的提升和转换可能在函数匹配时产生意想不到的结果,应尽量避免)

(4)如果重载函数的区别仅在于引用类型的形参是否引用了常量,或者指针类型的形参是否指向了常量,则在调用过程中通过实参是否是常量来决定选择哪个函数

7、函数指针

(1)函数指针指向函数而不是对象,函数指针指向某种特定类型,函数的类型由返回类型和形参类型共同决定,与函数名无关。

(2)想要声明一个可以指向函数的指针,只需要用指针替换函数名;或将函数名作为一个值使用时,该函数自动转换为指针

bool lengthCompare(const string &, const string &);
bool (*pf)(const string &, const string &); //pf指向函数的指针,pf两端的括号必不可少。
bool *pf(const string &, const string &); //声明一个函数pf,返回bool*
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价语句,&可选

(3)可以直接使用指向函数的指针调用函数,不需要提前解引用;指向不同函数类型的指针间不存在类型转换;也可为函数指针赋值nullptr或值为0的整型常量表达式,表示指针没有指向任何函数。

bool b1 = pf("hello","goodbye"); //调用lengthCompare函数
bool b2 = (*pf)("hello","goodbye"); //等价调用
bool b3 = lengthCompare("hello","goodbye"); //等价调用
pf = nullptr;
pf = 0;

(4)编译器通过指针类型决定选用哪个重载函数,指针类型必须与重载函数中的某一个精确匹配

void ff(unsigned int);
void ff(int*);
void (*pf)(unsigned int ) = ff;

(5)虽然不能定义函数类型的形参,但形参可以是指向函数的指针,或形参的函数类型直接转换为指针可以直接将函数作为实参使用,它会自动转换为指针

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, lengthCompare);

(6)使用typedefdecltype可以简化函数指针;decltype返回函数类型,,不会将函数类型自动转换成指针类型

//Func 和Func2为函数类型
typedef bool Func(const string &, const string &);
typedef decltype(lengthCompare) Func2;
//Funcp和Funcp2 指向函数的指针
typedef bool *Funcp(const string &, const string &);
typedef decltype(lengthCompare) *Funcp2;

(7)编译器不会自动将函数返回类型当成对应的指针类型处理必须将返回类型写成指针形式使用类型别名,声明一个返回函数指针的函数(也可以使用尾置返回类型)。

//类型别名
using F = int((int *,int *);//F是函数类型
using pF = int(*)((int *,int *);//pF是指针类型
//显式的将返回类型指定为指针
pF f1(int);
F *f1(int);
int (*f1(int))(int *, int);//由内到外阅读
//尾置返回类型
auto f1(int)-> int (*)(int * , int);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值