C++ Primer第七章函数笔记

1.函数形参

// 错,如果两个参数具有相同的类型,则其类型必须重复声明
int manip(int v1, v2){
}
int manip(int v1, int v2){
}

2.指针形参

void reset(int *ip){
	*ip = 0;
	ip = 0;
}

int i = 42;
int *p = &i;
// 输出i: 42
cout << "i: " << *p << '\n';
reset(p);
// 输出i: 0. *p变了,但是p指针没有变.因为形参只作用于函数,并不改变实际值
cout << "i: " << *p << endl;

// 可以将指向const对象的指针初始化为非const对象,但不可以让指向非const对象的指针指向const对象
// 既可以用int*也可以用const int*类型的实参调用use_ptr函数,但仅能将int*类型的实参传递给reset函数
// 因为可以将指向const对象的指针指向非const对象,但是不能让指向非const对象的指针指向const对象
// 因为指向const对象的指针不能改变其指向对象值,当指向非const对象时,则只要不通过指针改变非const对象值即可
// 指向非const对象的指针说明可以改变指向对象值,当指向const对象时,却不能改变,则报错
void use_ptr(cosnt int *p){
	...
}
  1. const形参
    调用函数时,形参只要是非引用的,则既可以给函数传递const实参也可以传递非const实参.
    1)调用函数时,如果函数使用非引用非const形参,则既可以给函数传递const实参也可以传递非const实参,因为函数使用的只是实参的副本不涉及实参本身的修改,故既可以是非const 也可以是const.
    2)如果形参是非引用的const类型,则在函数中不可以改变实参的局部副本,由于实参是以副本传递给形参的,故即可以传递const对象也可以是非const对象。 因为无论什么值都不会被修改,就都可以传递了。

复制实参(即调用非引用非指针函数,并将实参传入)有其局限性,以下几种不适合
1)当需要在函数中修改实参时
2)当传递大型对象时复制实参开销太大
3)没有办法实现对象复制时
以上几种情况,有效解决办法是将形参定义为引用或指针类型。

  1. 引用形参
// 使用引用形参,对引用形参的修改就是对相应实参的修改
void swap(int &v1, int &v2){
	...
}
int m = 10, n = 20;
// 对形参v1和v2的修改就是对m和n的修改
swap(m, n);

4.1 使用引用形参返回额外信息
函数只能返回单个值,当想调用某个函数返回多个值时,有以下两种方法:
1)在函数中传入一个额外的引用实参,用于返回需要的结果
2)定义一个包含多个返回信息的新类型

4.2 利用const引用避免复制
在向函数传递大型对象时,复制实参效率太低,且某些类类型无法复制,使用引用形参,函数可以直接访问实参对象,而无须复制.

// 字符串可能很大
// 如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为const引用
bool isShorter(const string &s1, const string &s2){
	return s1.size() < s2.size();
}

4.3
1)如果函数具有非const引用形参,则不能通过const对象进行调用,因为此时函数可以修改传递进来的对象,这样就违背了实参的const特性.
2)如果函数具有非const引用形参,传递一个右值(只能出现在等号右边的值,如数字字面值、字符串字面值)或具有需要转换的类型的对象同样不允许.
3)应该将不需要修改的引用形参定义为const引用.普通的非const引用形参在使用时不太灵活,这样的形参既不能用const对象初始化,也不能用字面值或产生右值的表达式实参初始化.

int incr(int &val){
	return ++val;
}
int main(){
	short vl = 0;
	const int v2 = 42;
	// 错, v1是short类型,需要转换
	int v3 = incr(v1);
	// 错, v2是const,而形参是非const
	v3 = incr(v2);
	// 错, 0 是右值
	v3 = incr(0);
	// 错, v1+v2是右值
	v3 = incr(v1+v2);
	// 对,方法是对的
	int v4 = incr(v3);
}
  1. 传递指向指针的引用
// v1、v2是指向指针的引用
void ptrswap(int *&v1, int *&v2){
	int *tmp = v2;
	v2 = v1;
	v1 = tmp;
}

int main(){
	int i = 10;
	int j = 20;
	int *pi = &i;
	int *pj = &j;
	ptrswap(pi, pj);
}
  1. vector和其它容器类型的形参
// 通常,函数不应该有vector或其它标准库容器类型的形参,调用含有普通的非引用vector形参的函数将会复制vector中的每一个元素
// c++程序员更倾向于传递指向容器中需要处理元素的迭代器来传递容器,当然将形参声明为引用类型也可以
void(vector<int>::const_iterator beg, vector<int>::const_iterator end){
	while(beg != end){
		cout << *beg++;
		if(beg != end) cout<< " "; 
	}
}
  1. 数组形参
// 以下三种定义是等价的,形参类型都是int*
// 虽然形参表示方式不同,但可将使用数组语法定义的形参看作指向数组元素类型的指针
void printValues(int *){...};
void printValues(int[]){...};
void printValues(int[10]){...};

int main(){
	const int i = 0, j[2] = {0,1};
	// 编译器忽略为任何数组形参执行的长度
	printValues(&i);
	printValues(j);
}

// 如果形参是数组的引用,编译器不会将数组实参转换为指针,而是传递数组的引用本身.
// 在这种情况下,数组大小称为实参和形参类型的一部分,编译器检查数组实参大小和形参大小是否匹配
// (&arr)括号是必要的,因为下标操作符具有更高的优先级
void printValues(int (&arr)[10]){
	...
}
int main(){
	int i=0, j[2] = {0,1};
	int k[10] = {0,1,2,3,4,5,6,7,8,9};
	// 错, 参数不是10个int类型大小的数组
	printValues(&i);
	// 错, 参数不是10个int类型大小的数组
	printValues(j);
	// 对
	printValues(k);
}

将一个数组名(实际为第一个参数指针)传入参数为非引用类型的形参指针的函数,改变形参不会改变实参指针的值,但是可以通过指针改变它所指向的数组的值.

  1. 多维数组的传递
// matrix[10]的优先级比*matrix高,首先得知matrix[10]是一个存放10个元素的数组,然后往左看是int *,说明数组中存放的是指针
int *matrix[10];
// 首先得知matrix是一个指针,往右看[10],得知是一个指向存放10个元素数组的指针,再往左看是int,所以matrix是一个指向存放10个int型元素的数组的指针
int (*matrix)[10];
// 与一维数组一样,编译器忽略第一维的长度
void printValues(int matrix[][10], int rowSize);
  1. 确保函数的操作不超出数组实参的边界三种方法
// 第一种方法: 数组本身放置一个标记来检测数组的结束,如C风格字符串以空字符null作为结束的标记
// 第二种方法: 传递指向数组的第一个和最后一个元素的下一个指针
// 参数说明beg和end指向的int型数是一个常量const,指针beg和end在函数中是可以变的
void printValues(const int *beg, const int *end){
	while(beg != end){
		cout << *beg++ <<endl;
	}
}

int main(){
	int j[2] = {0,1};
	printValues(j, j+2);
	return 0;
}
// 第三种方法: 定义一个形参表示数组的大小
void printValues(const int ia[], size_t size){
	for(size_t i = 0; i != size; ++i){
		cout << ia[i] <<endl;
	}
}

int main(){
	int j[] = {0,1};
	printValues(j, sizeof(j)/sizeof(*j));
	return 0;
}
  1. 省略符形参
// 当调用函数时,对与显示声明的形参相对应的实参进行类型检查,对于省略符对应的实参则暂停类型检查
void foo(parm_list,...);
// 当调用函数时,可以有0个或多个实参
void foo(...);
  1. return语句
    return语句用于结束当前正在执行的函数,并将控制权返回给调用此函数的函数.return语句有两种形式:return; return expression;
// 不带返回值的return语句(return;)只能用于返回类型为void的函数.在返回类型为void的函数中,return返回语句不是必须的.
void swap(int &v1, int &v2){
	if(v1 == v2){
		return;	
	}
	int tmp = v2;
	v2 = v1;
	v1 = tmp;
	// return;语句可写可不写
}

// 返回类型为void的函数可以返回另一个返回类型同样是void的函数的调用结果
void do_swap(int &v1, int &v2){
	int tmp = v2;
	v2 = v1;
	v1 = tmp;
	// return;语句可写可不写
}
void swap(int &v1, int &v2){
	if(v1 == v2){
		return;
	}
	return do_swap(v1, v2);
}

// 返回类型不是void的函数必须返回一个值有一个例外:允许主函数main没有返回值就可结束(隐式插入return 0;).
// 可将主函数main的返回值视为状态指示器,返回0表示程序运行成功,其它大部分值表示失败
// 非0返回值的意义因机器不同而不同,为了使返回值独立于机器,cstdlib头文件定义了两个预处理变量,分别用来表示程序运行成功和失败
#include<cstdlib>
int main(){
	if(some_failure)
		return EXIT_FAILURE;
	else
		return EXIT_SUCCESS;
}

// 不能返回局部对象的引用
// 同样不能返回指向局部对象的指针,一旦函数结束,局部对象被释放,返回的指针就变成了指向不再存在的对象的悬垂指针.
const string &manip(const string& s){
	string res = s;
	// res是局部对象,不能返回它的引用
	return ret;
}

char &get_val(string &str, string::size_type ix){
	return str[ix];	
}
int main(){
	string s("a value");
	cout << s << endl;
	// 返回引用的函数返回一个左值(可以出现在等号的左边或右边),因此,这样的函数可用于任何要求使用左值的地方.
	get_val(s, 0) = 'A'
	cout << s <<endl;
	return 0;
}

  1. 函数声明
    1)一个函数只能定义一次,但是可声明多次.
    2)函数声明由函数返回类型、函数名和形参列表组成.形参列表必须包括形参类型,但是不必对形参命名,这三个元素称为函数原型,函数原型描述了函数的接口.
    3)函数原型为定义函数的程序员和使用函数的程序员之间提供了接口,在使用函数时,程序员只对函数原型编程即可.
    4)同变量在头文件中声明,在源文件中定义一样,函数也应当在头文件中声明,在源文件中定义.
    5)把函数声明直接放到每个使用该函数的源文件中,合法,但这种用法比较呆板而且容易出错,解决方法是把函数声明放在头文件中,这样可以确保对于指定函数其所有声明保持一致.如果函数接口发生变化,只要修改其唯一的声明即可.
    6)定义函数的源文件应包含声明该函数的头文件.可使编译器检查该函数的定义和声明是否一致.

  2. 默认实参
    默认实参是通过给形参表中的形参提供明确的初始值来指定的,程序员可为一个或多个形参定义默认值.但是,如果有一个形参具有默认实参,那么它后面的所有形参都必须有默认实参.

// 既可以在函数声明也可以在函数定义中指定默认实参,但是,在一个文件中,只能为一个形参指定默认实参一次.
// 通常应该在函数声明中指定默认实参,并把该声明放在合适的头文件中
// 如果在函数定义的形参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才是有效的.
// 错误例子
// ff.h,不必对形参命名
int ff(int = 0);
// ff.cc
# include "ff.h"
int ff(int i = 0){...}
  1. 自动对象
    只有当定义它的函数被调用时才存在的对象称为自动对象.自动对象在每次函数调用时创建和撤销.
    局部变量和形参都是自动对象.
  2. 静态局部变量
    一个变量如果位于函数的作用域内,但生命期却跨越了这个函数的多次调用(函数调用结束,该变量依然存在),这种变量往往很有用,应该把这样的对象定义为static.
    静态局部变量一旦被创建,在程序结束前都不会被撤销.在包含静态变量的函数被多次调用的过程中,静态局部对象会持续存在并保持它的值.
size_t count_calls(){
	static size_t ctr = 0;
	return ++ctr;
} 
int main(){
	for(size_t i = 0; i != 10; ++i){
		cout << count_calls() <<endl;
	}
}
  1. 内联函数
    内联函数就是将它在程序中的每个调用点上“内联的”展开(即将函数源码替换调用函数)
const string &shorterString(const string &s1, const string &s2){
	return s1.size()<s2.size() ? s1:s2;
}
// 为这样的小操作定义一个函数的好处:
// 1)阅读理解函数比一条等价的条件表达式更加容易
// 2)修改函数比修改每一处容易
// 3)确保统一的行为
// 4)可以重用

// 为这样的小操作定义一个函数的缺点:
// 调用函数比求解等价表达式慢得多,因为调用函数要做很多工作:调用前要先保存寄存器,并在返回时恢复;复制实参;程序还必须转向一个新位置执行.

// 内联函数定义源码
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; 
// 内联函数的优点:
// 1)拥有函数具有的一切好处
// 2)消除了写成函数的额外执行开销

//内联函数适用场景: 适用于优化小的,只有几行的而且经常被调用的函数.
// 内联函数应该在头文件中定义,而一般函数在头文件中声明,在源文件中定义.

  1. 类的成员函数
// 函数原型必须在类中定义(即声明),但是,函数体既可以在类中也可以在类外定义,编译器隐式地将在类内定义的成员函数当作内联函数
class Sales_item{
	public:
		// 表示这是一个const成员函数,只能读不能改调用它们的对象的数据成员
		double avg_price() const;
		bool same_isbn(const Sales_item &rhs) const{
				return isbn == rhs.isbn;
				// this指针是指向被调用函数所属对象的指针,在该处则指向Sales_item类,不必显式的使用this指针
				return this->isbn == rhs.isbn;
		}
	private:
		std::string isbn;
		unsigned units_sold;
		double revenue;
};

//在类的外面定义成员函数必须指明它们是类的成员
double Sales_item::avg_price() const{
	if(units_sold){
		return revenue/units_sold;
	}
	else
		return 0;
};
  1. 构造函数
// 构造函数是特殊的成员函数,与其它成员函数不同的是,构造函数与类同名,而且没有返回类型
// 构造函数通常应确保其每个数据成员都完成了初始化
// 接上面例子,在Sales_item中定义构造函数
// isbn是string类类型对象,默认为“ ”,无需初始化
// 在冒号和花括号之间的代码称为构造函数的初始化列表,它跟在构造函数的形参表后,以冒号开头
// 构造函数的初始化式是一系列成员名,每个成员后面是括在圆括号中的初始值,多个成员的初始化用逗号分隔.
Sales_item():units_sold(0),revenue(0.0){...}

  1. 重载函数
    在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。不能仅通过返回类型的不同来重载函数.
  2. 重载和作用域
// 局部的声明一个函数,则该函数将会屏蔽而不是重载在外层作用域中声明的同名函数
void print(const string &);
void print(double);
void fooBar(int ival){
	// 屏蔽外面的print函数
	void print(int);
	// 错, string无法隐式转换为int类型
	print("Value: ");
	// 对
	print(ival);
	// 对, double隐式转换为int类型
	print(3.14);
}
  1. 参数匹配和枚举类型
// 整数对象即使具有与枚举类型相同的值也不能用于调用期望获得枚举类型实参的函数
enum Tokens(INLINE = 128, VIRTUAL = 129);
void ff(Tokens);
void ff(int);
int main(){
	Tokens curTok = INLINE;
	// 调用ff(int)
	ff(128);
	// 调用ff(Tokens)
	ff(INLINE);
	// 调用ff(Tokens)
	ff(curTok);

	// 可以将枚举值传递给整型形参
	void newf(unsigned char);
	void newf(int);
	unsigned char uc = 129;
	// 调用newf(int),虽然VIRTUAL=129可以用unsigned char类型表示,但是却不会将VIRTUAL提升为unsigned char类型,而是提升为int类型
	newf(VIRTUAL);
	// 调用newf(unsigned char)
	newf(uc);
	return 0;
}
  1. 重载和const形参
// 不能基于指针本身是否是const来实现函数的重载,下面两种情况都复制了指针,指针本身是否为const并没有带来区别
f(int *);
f(int *const);
  1. 指向函数的指针
    函数指针只能通过同类型的函数或函数指针或0值常量表达式进行初始化或赋值.
// pf为指向带有两个const string&类型的形参和bool类型的返回值的函数的指针
bool (*pf)(const string &, const string &);
// pf为返回bool*类型的函数
bool *pf(const string &, const string &);
// cmpFcn是一种指向函数的指针类型的名字,要使用这种函数指针类型时,直接使用cmpFcn即可.
typedef bool (*cmpFcn)(const string &, const string &);

// 在引用函数名但又没有调用该函数时,函数名将被自动解释为指向函数的指针
bool lengthCompare(const string &, cosnt string &);
// 定义一个空指针,并不指向任何函数
cmpFcn pf1 = 0;
// lengthCompare是一个指针,赋值给pf2,则pf2指针指向lengthCompare函数
cmoFcn pf2 = lengthCompare;
// lengthCompare是一个指针,赋值给pf1,则pf1指针指向lengthCompare函数
pf1 = lengthCompare;
pf2 = pf1;
// 直接引用函数名等效于在函数名上应用取地址操作符
cmpFcn pf1 = lengthCompare;
cmpFcn pf2 = &lengthCompare;

//指向函数的指针之间不存在转换
string::size_type sumLength(const string&, const string&);
bool cstringCompare(char*,char*);
cmpFcn pf;
// 错, pf指针指向函数的返回类型是bool,而sumLength返回类型是string::size_type
pf = sumLength;
// 错, 形参类型不一样
pf = cstringCompare;
// 对
pf = lengthCompare;

// 指向函数的指针可用于调用它所指向的函数,可以不使用解引用操作符
cmpFcn pf = lengthCompare;
// 直接调用
lengthCompare("hi", "bye");
// 通过指针调用
pf(“hi”, "bye");
// 通过指针的解引用调用
(*pf)(“hi”, "bye") 

// 函数指针形参
// 形参为函数的参数自动被当作指向函数的指针,下面两种等同
void useBigger(const string &, const string &, bool(const string &, const string &));
void useBigger(const string &, const string &, bool (*)(const string &, const string &));

// ff(int)为一个带有int型的形参的函数,该函数返回 int (*) (int*, int),即返回一个指针,所指向的函数返回int型并带有两个分别是int*型和int型的形参.
int (*ff(int))(int*, int);
// 同下
typedef int (*PF)(int*, int);
PF ff(int);

// 允许将形参定义为函数类型,但是函数的返回类型只能是指向函数的指针,而不能是函数
typedef int func(int*, int);
// 对, f1为一个具有函数类型形参的函数
void f1(func);
// 错, f2的返回类型是参数
func f2(int);
// 对, f3的返回类型是一个指向func函数的指针
func *f3(int);

// 指针的类型必须与重载函数的一个版本精确匹配,如果没有精确匹配的函数,则对该指针的初始化或赋值都将导致编译错误
extern void ff(vector<double>);
extern void ff(unsigned int);
// 对, pf1为指向无返回值且形参类型为unsigned int的函数的指针,与&ff完美匹配
void (*pf1)(unsigned int) == &ff;
// 错,int类型,既不是unsigned int,也不是vector<double>,不能精确匹配
void (*pf2)(int) = &ff;
// 错,指向函数的返回类型为double
double (*pf3)(vector<double>) = &ff;

发布了89 篇原创文章 · 获赞 0 · 访问量 1026

猜你喜欢

转载自blog.csdn.net/qq_26496077/article/details/103964831