C++Primer 第五版 ——《第六章》 函数

目录

实参是形参的初始值,但是并没有规定实参对形参的求值顺序 (183P)

函数的形参列表 

函数的返回类型不能是数组或函数类型, 但可以是指向数组或函数的指针 (184P)

局部以及全局变量的 作用域 (184P)

 函数只能定义一次,但可以声明多次 ( 186P)

含有函数声明的头文件应该被包含到定义函数的源文件中( 186P)

分离式编译 (186P)

指针作为形参(187P)

使用引用避免拷贝 (189P)

使用引用形参从一个函数返回多个值 (190P)

顶层 const 作为函数形参 (191P)

底层 const 作为函数形参 (191P)

尽量使用(const)常量引用 (192P)

数组形参(数组作为函数的参数)(193P)

使用三种方法将数组 传递给函数  (194P)

const 指针 (195P)

数组的引用作为形参  来绑定到数组上 (195P)

传递多维数组(195P)

 

给 main  函数传递数组 (196P)

使用 initializer_list 作为函数的形参 (197P)

返回类型和return语句 (200P)

函数返回局部变量的初始化规则 (201P)

不要返回局部变量的引用或者指针 (201P)

如果函数返回一个类类型指针、引用或对象,可以使用函数调用的结果来调用结果对象的成员 (202P)

返回引用的函数将返回一个左值 (202P)

函数返回花括号包围的值的列表(203P)

主函数 main 的返回值 、EXIT_SUCCESS、EXIT_FAILURE(203P)

函数返回一个数组的指针或引用 (205P)

使用尾置返回类型来 返回数组的指针 或者 引用(204P)

使用 decltype 来声明 函数返回一个数组的指针 (  206P )

定义重载函数 (  206P )

底层const 可以区别函数的重载、但顶层const 不可以(208P)

const_cast 和 重载 (在重载函数中最有用 209P)

调用重载函数的过程 (209P)

重载与作用域  &&  内存作用域中的名称隐藏外层作用域中的同名名称 (210P)

(局部变量不能作为)默认实参 (212P)

使用内敛函数避免函数的开销 (213P)

constexpr 函数 (214P)

把内联函数 和 constexpr 函数放在头文件内

assert 预处理宏 (215P)

NDEBUG 预处理变量 (216P)

当调用重载函数集中的形参都是可转换时 (217P)

确定重载函数中的候选和可行函数的过程 (217P)

 

实参类型匹配 (219P)

函数匹配和const 实参

函数指针 (221)

重载函数的指针 (222P)

函数指针作为形参 (222P)

返回指向函数的指针 (指针函数 223P)

 


函数是什么?

  •  函数可以有0个或多个参数,而且(通常)会产生一个结果。

如何执行函数?

  •  我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号, 它接受一个表达式,该表达式可以是函数或者指向函数的指针( 函数指针); 调用表达式的类型就是函数的返回类型。

int fact(int val)
{
	int ret = 1;
	while (val > 1)
	{
		// 等价于 ret *= val--;
		 ret = ret * val;  --val;
	}
	return ret;
}
int main()
{
	cout << "结果为:" << fact(5) << endl;
	system("pause");
	return 0;
}

调用一个函数会做两件事:

  • 一是,提供实参会初始化该函数对应的形参
  • 二是, 会将控制权交给被调用的函数, 此时主调函数暂时被中断,被调用函数开始执行。

 return 语句也完成两项工作:

  • 一是返回return 语句中的值(如果有的话),
  • 二是 将控制权从被调用函数转移回主调函数。 函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。
     

实参是形参的初始值,但是并没有规定实参对形参的求值顺序 (183P)


  • 注意: 尽管实参与形参存在对应关系, 但是并没有规定实参的求值顺序, 编译器能以任意可行的顺序对实参求值
  • 注意: 实参的类型必须与对应的形参类型匹配, 即 在初始化过程中初始值的类型也必须与初始化对象的类型匹配。
int fact(int val)
{
	int ret = 1;
	while (val > 1)
	{
		// 等价于 ret *= val--;
		ret = ret * val;  --val;
	}
	return ret;
}
int main()
{
	fact("hello"); //  实参类型不正确,因为不能将 const char*  转换成int,所以错误。
	system("pause");
	return 0;
}

 


函数的形参列表 


 注意 : 任意两个形参都不能同名, 而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字.

void max(int x)
{
	int x = 8;  
	cout << x << endl;

}
int main()
{
	max(9);
	system("pause");
	return 0;
}

编译后:

C++Primer 第五版 ——《第六章》 函数_第1张图片

 

 注意:  不管怎样, 是否设置未命名的形参并不影响调用时提供的实参数量。 即使某个形参不被函数使用, 也必须为它提供一个实参


函数的返回类型不能是数组或函数类型, 但可以是指向数组或函数的指针 (184P)


练习题6.1:

  • 可以说实参是形参的初始值, 第一个实参会初始化第一个形参, 第二个实参会初始化第二个形参,以此类推。
  • 形参是指在函数的参数列表中以声明符声明的, 实参指的是在调用该函数时,提供的实际的值,以初始化形参。

练习题6.2:

a, 函数的返回类型跟 return 返回值的类型不同

b,函数没有返回类型

c, 形参列表中两个形参同名

d,该函数没有函数体

练习题6.3:

int main()
{
	auto absoluteV = [=](int v) {return (v > 0) ? v : v * -1; };
	cout << absoluteV(-2) << endl;
	system("pause");
	return 0;
}

局部以及全局变量的 作用域 (184P)


局部变量的生存期取决于它定义的方式是是什么:

自动对象:

  • 我们把只存在于块执行期间的对象称为自动对象。当块的执行结束后, 块中创建的自动对象就变成未定义的了。
  •  形参是一种自动对象。 函数开始时为形参申请存储空间, 因为形参定义在函数体作用域之内, 所以一旦函数终止, 形参也就被销毁了。
  • 形参自动对象的初始化是由实参来进行的。
  • 对于函数体内的自动对象, 如果该变量本身含有初始值,就用这个初始值初始化,否则,执行默认初始化。如果该变量的类型内置类型,那么默认初始化是未定义的值。 如果该变量的值是类类型, 那么默认初始化的值,根据该类自己决定

局部静态对象:

  •  该对象在程序的执行路径第一次经过对象定义语句时初始化, 并且直到程序终止时才被销毁, 在此期间即使对象所在的函数结束执行也不会对它有所影响。

size_t count_calls()
{
	static size_t ctr; // 如果没有提供初始值,默认初始化为0
	return ++ctr;
}

int main()
{
	for (size_t i = 0; i != 10; ++i)
	{
		cout << count_calls() << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

练习题6.6:

  • 形参是一种普通自动对象,当执行一个函数时,首先对实参求值,然后 为形参申请内存空间 ,然后实参初始化形参, 当该函数终止后,形参分配的内存空间也将被销毁。
  • 局部变量也是一种普通的自动对象, 该对象在 执行路径经过定义语句时,创建该对象, 当到达定义所在的块末尾时销毁它。如果没有初始化这样的对象, 执行的是默认初始化。
  • 静态局部变量,  在程序的执行路径第一次经过对象定义语句时初始化,并直到程序终止时销毁。 在此期间即使该函数结束执行也不会对变量的值有所影响。 如果没有对该对象执行初始化, 将执行值初始化。
size_t count_calls(int val)
{
	int temp = 10 * val;
	cout << "输出temp的值:" << temp << endl;
	static size_t ctr = 22; 
	return ctr*val;
}

int main()
{
	cout << count_calls(5) << endl;
	cout << endl;
	system("pause");
	return 0;
}

练习题6.7:

size_t count_calls()
{
	static size_t ctr; 
	return ctr++;
}

int main()
{
	for (size_t i = 0; i != 10; ++i)
	{
		cout << count_calls() << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

 函数只能定义一次,但可以声明多次 ( 186P)


含有函数声明的头文件应该被包含到定义函数的源文件中( 186P)


分离式编译 (186P)



指针作为形参(187P)


当执行指针拷贝时,拷贝的是指针的值。 拷贝之后 , 两个指针是不同的指针; 即它们在内存中的地址是不相同的。因为指针可以间接地访问所指对象, 可以通过指针修改其所指对象的值。


void reset(int *ip)
{
	*ip = 10;
	ip = nullptr;
}

int main()
{
	int i = 220;
	reset(&i);
	cout << i << endl;
	system("pause");
	return 0;
}

练习题6.10:

void swap( int *q,  int *p)
{
	int temp = *q;
	*q = *p;
	*p = temp;
}

int main()
{
	auto swap_2=[&](int *q,int *p)
	{
		int temp = *q;
		*q = *p;
		*p = temp;
	};
	int i = 100;
	int ii = 22;
	swap_2(&i, &ii);
	cout << i << " " << ii << endl;
	system("pause");
	return 0;
}

使用引用避免拷贝 (189P)


  •  拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO 类型在内) 根本就不支持拷贝操作。
  • 当某种类型不支持拷贝操作时, 函数只能通过引用形参访问该类型对象。
  •  如果函数无须改变引用形参的值,最好将其声明为 常量(const)引用。

使用引用形参从一个函数返回多个值 (190P)


 一个函数只能返回一个值,可以使用引用形参为我们一次返回多个结果。


string::size_type find_char(const string &s, char c, string::size_type &occurs)
{
	auto ret = s.size();
	occurs = 0;
	for (decltype(ret) i = 0; i != s.size(); ++i)
	{
		if (s[i] == c)
		{
			if (ret == s.size())
			{
				ret = i; 
			}
			++occurs;
		}
	}
	return ret;
}
int main()
{
	string s = "hoong";
	string::size_type ctr = 0;
	auto index = find_char(s, 'o', ctr);
	cout << index << " " << ctr << endl;
	system("pause");
	return 0;
}
运行结果: 1、2

练习6.15:

  • 对于查找的字符串s来说, 为了避免拷贝字符串,使用引用类型;同时我们只执行查找操作,无须改变字符串的内容, 所以将其声明为常量引用
  • 对于待查找的字符c来说, 它的类型是char,只占1字节, 拷贝代价低,而且我们不需要改变实参的内容,只把值拷贝给形参即可, 所以不需要使用引用类型
  • 对于字符出现的次数 occurs 来说, 因为需要把函数内对实参值的更改反映在函数外部, 所以必须将其定义成引用类型, 但是不能把它定义成常量引用

顶层 const 作为函数形参 (191P)


int main()
{
	int i = 0;
	int *const p1 = &i; // 不能改变p1的值,是顶层 const
	const int ci = 42; // 不能改变ci的值,是顶层 const
	const int *p2 = &ci; // 允许改变p2 的值,底层const
	const int *const p3 = p2; // 左边底层const,右边顶层 const
	const int &r = ci; // 用于声明引用的const 都是底层 const
	
	system("pause");
	return 0;
}

当一个函数的形参是顶层 const 时, 传入的值是 常量 还是 非常量 都是无关紧要的。

void fcn(const int i)
{ 
	cout << "调用的是 fcn: const int" << endl;
}
void fcn(int i)
{
	cout << "调用的是 fcn: int" << endl;

}
int main()
{
	const int i = 9;
	fcn(i);
	
	system("pause");
	return 0;
}

C++Primer 第五版 ——《第六章》 函数_第2张图片

说明: 在 C++ 语言中,允许我们定义若干具有相同名字的函数, 不过前提是不同函数的形参列表应该有明显的区别。

因为顶层const被忽略掉了, 所以在上面的代码中传入两个fun函数的参数是完全一样的。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。


底层 const 作为函数形参 (191P)


  •  我们可以使用非常量初始化一个底层 const 对象,
  • 但是一个底层const 不能初始化一个非常量对象;
  • 而且,同时一个普通的引用必须用同类型的对象初始化。
int main()
{
	int i = 55;
	const int *cp = &i; // 允许改变cp的值,底层const,但是不能改变所指对象
	const int &r = i; // 底层 const,不能改变所指对象
	const int &r2 = 42; // 底层 const

	int *p = cp; // 错误,如果p 指向cp,就相当于指向了i , 因为cp i 不能改变i, 所以p 指向i的话,可能会修改其值。所以类型不匹配
	int &r3 = r; // 因为 r是 const, 如果 r3 指向r 可能会修改其值, 所以类型不匹配
	int &r4 = 55; // 不能将一个字面值初始化一个非常量引用

	system("pause");
	return 0;
}
void reset(int &i)
{
	cout << "调用的是 int &i" << endl;
}

void reset (int *i)
{
	cout << "调用的是 int *i" << endl;
}

string::size_type find_char(const string &s, char c, string::size_type &occurs)

{
	cout << "调用的是 find_char" << endl;
}
int main()
{
	int i = 55;
	const int ci = i;
	string::size_type ctr = 0;

	reset(&i); // 调用的形参是 int* 的 reset 函数
	reset(&ci); // 不能将一个 普通指针指向一个 const int 对象

	reset(i); // 调用 形参类型是 int& 的reset 函数
	reset(ci); // 不能将一个普通引用绑定到一个 const 对象上
	reset(455); // 不能将一个字面值绑定在一个非常量引用上
	find_char("Hello World!", 'o', ctr); // 正确 该函数的第一个形参是 const 的引用
	system("pause");
	return 0;
}

 要想调用引用版本的reset  只能使用:

  • int 类型的对象, 而不能使用字面值、求值结果为int的表达式、需要转换的对象或者 是 const int 类型的对象。

类似的,要想调用指针版本的 reset 只能使用int*, 我们能传递一个字符串字面值作为 find_char () 函数的第一个实参,这是因为该函数的引用形参是常量引用。 注意:  C++ 允许我们用字面值初始化常量引用

const int &a=42;

 


尽量使用(const)常量引用 (192P)


  • 把函数不会改变的形参定义成普通的引用是一种比较常见的错误;
  • 然而使用普通引用而非const引用也会极大地限制函数所能接受的实参类型。
  • 因为我们不能把 const对象、字面值 或者 需要类型转换的对象 传递给普通的引用形参; 但是 const 引用 可以使用这些值作初始化。
string::size_type find_char( string &s, char c, string::size_type &occurs)

{
	cout << "调用的是 find_char" << endl;
}
int main()
{
	string::size_type ctr = 9;
	find_char("Hello World!", 'o', ctr); // 
	system("pause");
	return 0;
}

C++Primer 第五版 ——《第六章》 函数_第3张图片

string::size_type find_char( string &s, char c, string::size_type &occurs)

{
	cout << "调用的是 find_char" << endl;
	return 0;
}
bool is_sentence(const string &s)
{

	string::size_type ctr = 0;
	string temp = s;
	return find_char(temp, '.', ctr) == s.size() - 1 && ctr == 1;
}
int main()
{
	string::size_type ctr = 9;
	string tep = "Hello World!";
	find_char(tep, 'o', ctr); 
	system("pause");
	return 0;
}

练习题6.17:


bool find_char(const string &v)
{
	if (!v.empty())
	{
		for (const auto tt : v)
		{
			if (isupper(tt))
			{
				return true;
			}
		}
	}
	return false;
}
void changeToLower(string &s)
{
	if (!s.empty())
	{
		for (auto &tt : s)
		{
			tt = tolower(tt);
		}
	}
}
int main()
{
	string temp = "HUANG";
	if (find_char(temp))
	{
		cout << "该函数中有大写字母" << endl;
	}
	else
		cout << "该函数中没有大写字母" << endl;
	changeToLower(temp);
	for (auto tt : temp)
	{
		cout << tt << " ";
	}
	system("pause");
	return 0;
}

练习题:6.18:

  • bool compare ( const matrix& , const matrix&)
  • vector change_val (int, vector< int>)

练习6.19:

a, 不合法,实参数量过多

b,合法

c,合法,会int 转换为 double

d, 合法

练习题6.20:

  • 当该函数不需要修改形参的值时, 使用 const 引用, 否则 ; 使用普通引用。
  • 如果形参应该是 const 引用,而设为 const引用, 可能会调用者误导,即该函数的形参可以被改变。 再次普通引用会限制函数所能接受的实参类型。

数组形参(数组作为函数的参数)(193P)


数组有两个特殊属性,它们影响我们如何定义和使用操作数组的函数:这两个性质分别是:

  • 不允许拷贝数组 (但是可以使用 数组的引用(195P))
  • 以及使用数组时通常会将其转换成指针。
  • 因为不能拷贝数组,所以我们无法按值传递数组
  • 因为数组会被转换成指针, 所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
  • 注意: 和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
void print(const int*p){}
int main()
{
	int i = 0, j[2]{ 0,1 };
	print(&i); // &i 的类型是int*
	print(j);// j 转换成int* 并指向j[0]
	system("pause");
	return 0;
}

当一个函数的形参是一个数组的形式或者指针, 但把数组传递给它们时,则实参会自动地转换成指向数组首元素的指针,数组的大小是无关紧要的。 说白了就是数组是以指针的形式传递给函数的


使用三种方法将数组 传递给函数  (194P)


void print(const char *cp)
{

  if (cp)  //若cp不是一个空指针
	while (*cp)  //只要指针所指的字符不是空字符
	  cout << *cp++; //输出当前的元素,然后指针向前移动一位
}
int main()
{
	print("huang");
	system("pause");
	return 0;
}
#include
using namespace std;

void print(const int *beg, const int *end)
{            //输出beg到end之间(不包含 end) 的所有元素
    while (beg != end)
    {
        cout << *beg++ << endl;
    }
}
int main()
{
    int j[2] = { 0,1 };
     //j 转换成指向它首元素的指针
    //第二个实参是指向j 的尾后元素的指针
    print(begin(j), end(j));

    system("pause");
    return 0;
}
#include
using namespace std;

//size  表示数组的大小, 将它显式地传给函数用于控制对 ia 元素的访问
void print(const int ia[],size_t size)
{   

    for (size_t i = 0; i != size; ++i)
    {
        cout << ia[i] << endl;
    }
}
int main()
{
    int j[2] = { 0,1 };
    print(j, end(j) - begin(j));
    system("pause");
    return 0;
}

const 指针 (195P)



数组的引用作为形参  来绑定到数组上 (195P)


void print(int(&arr)[10])
{
	for (auto elem : arr)
	{
		cout << elem << " ";
	}
	cout << endl;
}
int main()
{
	int i = 0, j[2] = { 0,1 };
	int k[] = { 0,1,2,3,4,5,6,7,8,9 };
	print(&i);//错误,实参不是一个含有10个元素的数组
	print(j);//错误,实参不是一个含有10个元素的数组
	print(k); //正确
	system("pause");
	return 0;
}

但是传递给print 函数的数组只能是大小为10的数组。


传递多维数组(195P)


当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。

 因为我们传递的是数组的数组, 所以首元素本身就是一个数组, 指针就是一个指向数组的 指针。

数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:

void  print (int (*matrix)[10],int rowSize); // matrix 指向数组的首元素, 该数组的元素是由10个整数构成的数组

等价定义

void print(int matrix[][10],int rowSize);

上述语句将matrix 声明成指向含有10 个整数的数组的指针。


int *matrix[10] ; //10个指针构成的数组
int (*matrix)[10]; // 指向含有10个元素的数组的指针

练习题6.21:

int main()
{
	auto compare = [](const int v1, const int *const v2) {return (v1 < *v2) ? *v2 : v1; };
	int i = 10;
	int j = 20, *tes = &j;
	cout << "较大的为:" << compare(i, &j) << endl;
	system("pause");
	return 0;
}

练习题6.22:

int main()
{
	auto swap_v = [&](int *p,int*q) 
	{
		int temp = *p;
		*p = *q;
		*q = temp;
	};
	int i = 100, j = 99;
	swap_v(&i, &j);
	cout << "交换后的值为:" << i << " " << j << endl;
	system("pause");
	return 0;
}

 


给 main  函数传递数组 (196P)


使用 initializer_list 作为函数的形参 (197P)


  •   如果 函数的实参数量未知但是全部实参的类型相同,此时可以使用initialize_list 类型的形参,它是一种标准库类型。 
  • 用于表示某种特定类型的值的数组。 该类型定在的 #include 头文件中。

 

注意: initialize_list 也是一种模板类型,定义 initialize_list 对象时, 必须说明列表中所含的元素类型,列如:

 initialize_list    ls;  // initialize_list  的元素类型是string
  • 注意 : 和 vector 不一样的 是, initialize_list 对象中的元素永远都是常量值, 我们无法修改 initialize_list 对象中元素的值。
  • 注意: 如果想向` initialize_list  `形参中传递一个值的序列,则必须把序列放在一对花括号内
  • 注意 : 含有initialize_list 形参的函数也可以同时拥有其他形参
void error_msg(initializer_list il)
{
	for (auto beg = il.begin(); beg != il.end(); ++beg)
	{
		cout << *beg << " ";
	}
}

int main()
{
	string expected = "huang";
	string actual = "cheng";
	if (expected != actual)
	{
     //如果想向initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内。
		error_msg({ "functionX",expected,actual }); 
	}
	else
		error_msg({ "functionX","okay" });
	system("pause");
	return 0;
}


练习题6.27:

int error_msg(initializer_list il)
{
	int count = 0;
	for (auto beg = il.begin(); beg != il.end(); ++beg)
	{
		count += *beg;
	}
	return count;
}

int main()
{
	cout << "总和为:" << error_msg({ 6,1,2,34,56 }) << endl;
	system("pause");
	return 0;
}

练习题6.28:

  • initializer_list 的所有元素类型都是 string, 因此 const auto &elem : il 推断得到的 elem 的类型是const string&。使用引用是为了避免拷贝长字符串,把它定义为常量的原因是我们只需读取字符串的内容,不需要修改它。

练习题6.29:

  • 不需要,因为 initializer_list 对象中的值都是常量值 , 所以我们不可以使用 引用来修改循环控制变量的值。

返回类型和return语句 (200P)


函数返回局部变量的初始化规则 (201P)


选出两个string对象中较短的那个,返回起引用
const string &shorterString(const string &s1, const string &s2)
{
	return  s1.size() <= s2.size() ? s1 : s2;
}

int main()
{

	const string temp = shorterString("huang", "cheng");
	cout << "输出结果为:" << temp << endl;
	system("pause");
	return 0;
}

不要返回局部变量的引用或者指针 (201P)


函数执行完后,它所占用的存储空间也随之被释放掉了。 所以函数终止了意味着局部变量的引用将指向不再有效的内存区域。

const string &manip()  //严重错误,这个函数试图返回局部变量的引用
{
	string ret;

	if (!ret.empty())
		return ret;      // 错误: 返回局部对象的引用
	else
		return "Empty";   //错误: “Empty”  是一个局部临时量
}

int main()
{

	const auto temp = manip();
	cout << "输出结果为:" << temp << endl;
	system("pause");
	return 0;
}

注意 : 返回局部对象的引用时错误的; 同样,返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。


如果函数返回一个类类型指针、引用或对象,可以使用函数调用的结果来调用结果对象的成员 (202P)


const string &shorterString(const string &s1, const string &s2)
{
    return  s1.size() <= s2.size() ? s1 : s2;
}

int main()
{
    string s1 = "huang", s2 = "tt";

    //我们可以通过这样的形式得到较短的string对象的长度
    auto temp = shorterString(s1, s2).size(); //调用string 对象的size成员, 该string对象是由shorterString 函数返回的
    cout << "输出结果为:" << temp << endl;
    system("pause");
    return 0;
}

返回引用的函数将返回一个左值 (202P)


 注意 : 一个函数的返回类型决定函数调用是否是左值。 调用一个返回引用的函数得到左值, 其他返回类型得到右值。

  •  可以像使用其他左值那样来使用返回引用的函数的调用
  •  特别是, 我们能为返回类型是非常量引用的函数的结果赋值
char &get_val(string &str, string::size_type ix)
{
	return str[ix];  //get_val 假定索引值是有效的
}

int main()
{
	string s("a value");
	cout << "输出s的结果:" << s << endl;

	get_val(s, 0) = 'A';  //将s[0] 的值改为 A, 因为该函数返回引用,所以可以是左值,其他返回类型得右值
	cout << "输出修改后s的值:" << s << endl;
	system("pause");
	return 0;
}

输出结果为:

输出s的结果:a value
输出修改后s的值:A value

该函数的返回值是引用,因此调用是个左值,和其它左值一样它可以出现在赋值运算符的左侧。

注意: 如果返回类型是常量引用, 我们就不能给调用的结果赋值,比如给上述函数加上const, 就不能给该函数赋新值。

const string &shorterString(const string &s1, const string &s2)
{
	return  s1.size() <= s2.size() ? s1 : s2;
}

int main()
{
	const string s1 = "huang", s2 = "tt";

	//我们可以通过这样的形式得到较短的string对象的长度
	auto temp = shorterString(s1, s2).size(); //调用string 对象的size成员, 该string对象是由shorterString 函数返回的
	cout << "输出结果为:" << temp << endl;
	shorterString("hi", "byte") = "x"; //不能给调用的结果赋值,因为该函数返回的是常量的引用,返回值是一个常量
	system("pause");
	return 0;
}

函数返回花括号包围的值的列表(203P)


  •  C++ 新标准规定, 函数可以返回花括号包围的值的列表。类似于其他返回结果, 此处的列表也用来对表示函数返回的临时量进行初始化。
  • 如果列表为空, 临时量执行中值初始化; 否则, 返回的值由函数的返回类型决定。
vector< string> process()
{
	string expected = "huang";
	string actual = "cheng";
	if (expected.empty())
	{
		return {}; //返回一个空的vector对象
	}
	else if (expected == actual)
	{
		return { "functionX","okay" }; // 返回列表初始化的vector对象
	}
	else
	{
		return { "functionX",expected,actual };
	}
}


int main()
{
	vector temp = process();
	for (auto t = temp.begin(); t != temp.end(); ++t)
	{
		cout << *t << " ";
	}
	system("pause");
	return 0;
}

输出结果为:
functionX huang cheng
int temp()
{
    return { 6 }; // 因为是内置类型,所以只能返回一个值
}

int main()
{
    int a = temp(); 
    cout << "输出结果:" << a << endl;
    system("pause");
    return 0;
}

注意 : 如果函数返回的是内置类型,则花括号包围得列表最多包含一个值, 而且该值所占空间不应该大于目标类型的空间。

如果函数返回的是类类型, 由类本身定义初始值如何使用


主函数 main 的返回值 、EXIT_SUCCESS、EXIT_FAILURE(203P)


注意 : 如果函数的返回类型不是 void ,那么它必须返回一个值。 但是这条规定有个例外:

  • 我们允许main 函数没有return 语句直接结束。如果控制到达了main 函数的结尾处而且没有return 语句,编译器将隐式地插入一条返回0的return语句。

cstdlib 头文件定义了两个预处理变量,我们使用这两个变量分别表示与失败:

#include
int fun(int a)
{
	if (a)
	{
		return EXIT_SUCCESS; //返回成功,即返回0
	}
	else
		return EXIT_FAILURE;
}

int main()
{
	auto tt = fun(5);
	cout << tt << endl;
	system("pause");
	return 0;
}
  • 因为它们是预处理变量,所以既不能在前面加上 std:: , 也不能 在 using 声明中 出现。
  • 注意: main() 函数不能调用它自己,不能实现递归。

练习题6.30:

  • 如果引用所引的是函数开始之前就已经存在的对象, 则返回该引用是有效的;
  • 如果引用所引的是函数的局部变量, 则随着函数结束局部变量也失效了,此时返回的引用无效。
  • 当不希望返回的对象被修改时, 返回对常量的引用。

练习题6.32:

  • 调用合法,  给数组初始化值为0 到9

函数返回一个数组的指针或引用 (205P)


注意 : 因为数组不能被拷贝,所以函数不能返回数组。 不过函数可以返回数组的指针或引用, 其中最直接的方法是使用类型别名。

typedef int arrT[10]; // arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT=int[10];  // arrT的等价声明
arrT *func(int i) //func 返回一个指向含有10个整数的数组的指针
{

}

 如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名之后。然而,函数的形参列表也跟在函数名字后面并且形参列表应该先于数组的维度。

Type(*function (parameter_list))
int (*func(int i))[10] ; //返回数组指针的函数

注意 : Type(*function (parameter_list)) 两端的括号必须存在, 如果没有括号, 函数的返回类型将是指针的数组——意识就是说它会返回一组指针。


使用尾置返回类型来 返回数组的指针 或者 引用(204P)


 在C++11 新标准中还有一种可以简化上述func 声明 的方法, 就是使用尾置返回类型。 任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效, 比如返回类型是 数组的指针 或者 数组的引用。

尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto

//func 接受一个int 类型的实参,返回一个指针, 该指针指向含有10个整数的数组
auto func(int i)  -> int(*)[10];

使用 decltype 来声明 函数返回一个数组的指针 (  206P )


定义重载函数 (  206P )


当调用一组重载函数时, 编译器会根据传递的实参类型推断希望调用哪个函数。

void print(const char *cp)
{
	if (cp) //若cp不是个空指针
	{
        // C风格的字符串的最后一个字符的后面还有一个空字符
		while (*cp) //该指针指向的不是空字符
		{
			cout << *cp++;
		}
		cout << endl;
	}
}
void print(const int *beg, const int *end)
{
	while (beg != end)
	{
		cout << *beg++;
	}
	cout << endl;
	/*for (auto tt = beg; tt != end; ++tt)
	{
		cout << *tt;
	}
	cout << endl;*/
}
void print(const int ia[], size_t size)
{
	for (size_t i = 0; i != size; ++i)
	{
		cout << ia[i];
	}
	cout << endl;
}
int main()
{
	int j[2] = { 0,1 };
	print("Hello World");
	print(j, end(j) - begin(j));
	print(begin(j), end(j));
	system("pause");
	return 0;
}

当两个重载函数只有返回类型不同 ,其余的都相同。 那么第二个同名函数错误。

注意: main()函数不能重载。


底层const 可以区别函数的重载、但顶层const 不可以(208P)


 注意 : 顶层const 不影响传入函数的对象。 一个拥有 顶层 const 的形参无法和另一个没有顶层const的形参区分开来。

//都错误,都重复声明
int lookup(int a); 
int lookup(const int a);

int lookup(int *);
int lookup(int *const);

另一方面, 如果形参是某种类型的指针或引用, 则通过区分其指向的是常量对象还是非常量对象可以实现函数重载, 此时的 const 是底层的:

// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了四个独立的重载函数
int lookup(int &);
int lookup(const int &);  //作用于常量引用

int lookup(int *);
int lookup(const int *); // 指向常量的指针

上面的示例代码,编译器可以通过实参是否是常量来推断应该调用哪个函数。

  • 因为const不能转换成其他类型, 所以我们只能把const对象(或指向 const的指针) 传递给const形参.
  • 所以上面的4个函数都能作用于非常量对象或者指向非常量对象的指针。
  • 注意: 如果我们传递一个非常量对象或者指向非常两对象的指针时,编译器会优先选用非常量版本的函数。

const_cast 和 重载 (在重载函数中最有用 209P)


const string &shorterString(const string &s1, const string &s2)
{
    return  s1.size() <= s2.size() ? s1 : s2;
}

函数的参数和返回类型都是 const string 引用, 当我们使用两个非常量的string实参调用这个函数时, 返回结果仍然为const string 的引用。

如果我们需要该函数但实参不是常量的时候 , 返回结果是一个普通的引用, 使用const_cast 可以做到这一点。

const string &shorterString(const string &s1, const string &s2)
{
	return  s1.size() <= s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2)  //先执行该函数
{
	auto &r = shorterString(const_cast(s1), // 在执行上面的函数
		const_cast(s2));
	return const_cast(r); // 返回主函数
}
int main()
{
	string s1 = "huang", s2 = "tt";

	//我们可以通过这样的形式得到较短的string对象的长度
	auto temp = shorterString(s1, s2).size(); //调用string 对象的size成员, 该string对象是由shorterString 函数返回的
	cout << "输出结果为:" << temp << endl;
	
	system("pause");
	return 0;
}

调用重载函数的过程 (209P)


调用重载函数的时候,编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。


重载与作用域  &&  内存作用域中的名称隐藏外层作用域中的同名名称 (210P)


重载对作用域的一般性质并没有什么改变: 如果我们在内层作用域中声明名字, 它将隐藏外层作用域中声明的同名实体。 在不同的作用域中无法重载函数名。

string read();
void print(const string &);
void print(double);
void fooBar(int ival)
{
	bool read = false; //隐藏了外层的read
	string s = read(); //错误, read是一个布尔值,而非函数
	//最好不要在局部作用域中声明函数
	void print(int);  // 隐藏了之前的print
	print("Value: "); //错误 print(const string &) 被隐藏掉了
	print(ival); //正确:print(int)可见
	print(3.14); // 正确,调用print(int) ,print(const string &) 被隐藏掉了
}

注意: 在C++中名字查找发生在类型检查之前。


(局部变量不能作为)默认实参 (212P)


 函数只声明一次,但是多次声明同一个函数也是合法的。 不过有一点需要注意, 在给定的作用域中一个形参只能被赋予一次默认实参。

 通常, 应该在函数声明中指定默认实参, 并将该声明放在合适的头文件中。

 注意 : 局部变量不能作为默认实参。 除此之外, 只要表达式的类型能转换成形参所需的类型, 该表达式就能作为默认实参。

 


使用内敛函数避免函数的开销 (213P)


注意: 内联说明只是向编译器发出的一个请求, 编译器可以选择忽略这个请求。

注意 : 内联函数可以避免函数调用的开销。一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个几十行上百行的代码也不大可能在调用点内联的展开。


constexpr 函数 (214P)


constexpr 函数 是 指用于常量表达式的函数。 不过要注意的是:

  • 函数的返回类型及所有形参的类型都是字面值类型。(一般来说基本的数据类型、引用和指针都属于字面值类型,自定义的类、string 不属于字面值,不可以定义为constexpr。)
  • 而且函数体中必须有且只有一条return语句。
constexpr int new_sz()
{
	return 42+10;
}
int main()
{

	constexpr auto foo = new_sz(); //正确:foo 是一个常量表达式

	cout << foo << endl;
	system("pause");
	return EXIT_SUCCESS;
}

在调用constexpr 函数时,为了能在编译过程中随时展开, constexpr 函数会被隐式地指定为内联函数。

注意 : constexpr 函数体内也可以包含其他语句, 只要这些语句在运行时不执行任何操作就行。 例如: constexpr 函数中可以有 空语句、类型别名 以及 using 声明.
 

 注意 : 我们允许constexpr 函数的返回值并非一个常量。 constexpr 函数不一定返回常量表达式

constexpr int new_sz()
{
    return 42;
}
constexpr size_t scale(size_t cnt)
{
    return new_sz()*cnt;
}
int main()
{
    constexpr int foo = new_sz();

    int arr[scale(2)] = {};  //正确: scale(2) 是常量表达式, 当提供的实参是常量表达式,其返回值也是表达式

     int i = 2;  //i不是常量表达式
    int a2[scale(i)];   // 错误:  scale(i) 不是常量表达式
    system("pause");
    return 0; 
}

把内联函数 和 constexpr 函数放在头文件内


  •  和 其他函数不一样, 内联函数和constexpr 函数可以在程序中多次定义。 毕竟, 编译器要想展开函数仅有函数声明是不够的, 还需要函数的定义。不过,对于某个给定的内联函数或者constexpr 函数来说, 它的多个定义必须完全一致。
  • 基于这个原因, 内联函数和 constexpr 函数通常定义在头文件中。

练习题6.43:

  • (a) 应该放在头文件中。因为内联函数的定义对编译器而言必须是可见的, 以便扁译器能够在调用点内联展开该函数的代码, 所以仅有函数的原型是不够的。并且, 与一般函数不同的是, 内联函数有可能在程序中定义不止一次, 此时必须保证在所有源文件中定义完全相同, 把内联函数的定义放在头文件中可以确保这一点。
  • (b)是函数声明,应该放在头文件中。

练习题6.46:

  •  不可以,因为该函数的返回类型是string, constexpr 函数必须返回字面值类型, 但是 string 并不是字面值类型。 

assert 预处理宏 (215P)


  •  assert 是一种预处理宏。 所谓预处理宏其实是一个预处理变量。它定义在头文件 cassert 中。
  •  注意 : 预处理名字由预处理器而非编译器管理, 因此我们可以直接使用预处理名字而无须提供 using 声明。
  • 和预处理变量一样,如果一个程序中含有 cassert 头文件, 那么该程序就不应再定义名为 assert 的变量 、函数或者其他实体。
  • 注意 : assert 宏 常用于检查“不能发生”的条件。

NDEBUG 预处理变量 (216P)


 assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。 如果定义了NDEBUG, 则 asset 什么也不做。在默认状态下没有定义 NDEBUG, 此时 assert 将执行运行时检查。

 我们可以使用 一个 #define 语句定义 NDEBUG ,从而关闭调试状态。

 定义NDEBUG 能避免检查各种条件所需的运行时开销。因此,assert 应该用于验证那些确实不可能发生的事情。

 除了用于 assert 外, 也可以使用 NDEBUG 编写自己的条件调试代码。 如果NDEBUG未定义, 将执行 #ifndef 和 #endif 之间的代码; 如果定义了NDEBUG ,这些代码将被忽略掉。

void print(const int ia[], size_t size)
{
#ifndef NDEBUG

	// __func__ 编译器定义的一个局部静态变量, 用于存放函数的名字
	cerr << __func__ << ": array size is " << size << endl;

#endif
}
int main()
{
	int arr[3] = { 0,1,2};
	print(arr, 3);
	system("pause");
	return 0;
}

当调用重载函数集中的形参都是可转换时 (217P)


 当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时, 就不容易确定某次调用应该选用哪个重载函数。

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
int main()
{
	f(5.6); //调用 f(double, double = 3.14)
	system("pause");
	return 0;
}

确定重载函数中的候选和可行函数的过程 (217P)


 函数匹配的第一步: 首先是选定本次调用对应的重载函数集, 集合中的函数称为候选函数。

候选函数具备两个特征:

  •   与被调用的函数同名,
  •  其声明在调用点可见。

 函数匹配的第二步: 考察本次调用提供的实参, 然后从候选函数中选出能被这组实参调用的函数, 这些新选出的函数称为可行函数

可行函数也有两个特征:

  • 其形参数量与本次调用提供的实参数量相等
  •  每个实参的类型与对应的形参类型相同, 或者能转换成形参的类型。

函数匹配的第三步(寻找最佳匹配(如果有的话)):

  • 从 可行函数中选择与本次调用最匹配的函数。 在这一过程中, 逐一检查函数调用提供的实参, 寻找形参类型与实参类型最匹配的那个 可行函数。
  • 它的基本思想是: 实参类型与形参类型越接近, 它们匹配得越好 精确匹配比需要类型转换的匹配更好。

 

实参类型匹配 (219P)



函数匹配和const 实参


  • 在分析调用函数前,小整型一般都会提升到int 类型或更大的整数类型。
  • 如果重载函数的区别在于它们的引用类型的形参是否引用了const, 或者指针类型的形参是否指向const, 则当调用发生时编译器通过实参是否是常量决定选择哪个函数。
  • 指针类型的形参也是类似。 如果两个函数的唯一区别是它的指针形参指向常量或非常量,则编译器能通过实参是否是常量决定选用哪个函数:
  • 如果实参是指向常量的指针,调用形参是const * 函数; 如果实参是指向非常量的指针, 调用形参是普通指针的函数。

函数指针 (221)


 函数指针指向的是函数而非对象。 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

 当把 函数名作为一个值使用时, 该函数自动地转换成指针。

 我们还能直接使用指向函数的指针调用该函数, 无须提前解引用指针:

 string lengthCompare(const string &s1, const string &s2)
{
    return s1.size() < s2.size() ? s2 : s1;
}
int main()
{
    string(*pf)(const string &s3, const string &s4); //定义函数指针pf
    pf = lengthCompare;  //pf指向名为lengthCompare 的函数

   //也可以这样把函数指针的声明和初始化写在一起
  //string(*pf)(const string &s3, const string &s4) = lengthCompare; //定义函数指针pf

    //pf = &lengthCompare;  //等价的赋值语句: 取地址赋是可选的
    cout << "请输入两个字符串:\n";
    string s1, s2;
    cin >> s1 >> s2;


    string s3 = (*pf)(s1, s2);  //调用lengthCompare函数
    //string s4 = pf(s1,s2);  一个等价的调用
    //string s5=lengthCompare(s1,s2); //另一个 等价的调用

    cout << "输出较长的字符串长度为:" << s3 << endl;
    system("pause");
    return 0; 
}
输出结果:
请输入两个字符串:
huan
ss
输出较长的字符串长度为:huan

 在指向不同函数类型的指针间不存在转换规则。但是,我们可以为函数指针赋一个nullptr 或者 值为0的整型常量表达式, 表示该指针没有指向任何一个函数:

string lengthCompare(const string &s1, const string &s2)
{
	return s1.size() < s2.size() ? s2 : s1;
}
string::size_type sumLength(const string &, const string &);

string cstringCompare(const string*, const string*);

int main()
{
	string(*pf)(const string &s3, const string &s4); //定义函数指针pf

	pf = 0; //正确: pf不指向任何函数

	pf = sumLength;  //错误: 函数返回类型不匹配

	pf = cstringCompare;  //错误: 函数形参类型不匹配

	pf = lengthCompare; //正确: 函数和指针的类型精确匹配

	system("pause");
	return 0;
}

重载函数的指针 (222P)


 当我们使用重载函数时, 上下文必须清晰地界定到底应该选用哪个函数。 如果定义了指向重载函数的指针。编译器会通过指针类型决定选用哪个函数, 函数指针的形参列表与重载函数中的某一个形参列表精确匹配。

void ff(int *);  //声明两个重载函数
void ff(unsigned int);

int main()
{
    void(*pf1)(unsigned int) = ff;  //pf1 指向 ff(unsigned int)

    void(*pf2)(int) = ff; //错误:  没有任何一个ff与该形参列表匹配

    double(*pf3)(int *) = ff; //ff和pf3 的返回类型 不匹配

    system("pause");
    return 0; 
}

函数指针作为形参 (222P)


虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数类型,实际上却是当成指针使用:

string lengthCompare(const string &s1, const string &s2)
{
    return s1.size() < s2.size() ? s2 : s1;
}

//第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2, string pf(const string &, const string &))
{
    // 一些语句
}

// 一个等价的声明,显式地将形参定义成指向函数的指针
/*void useBigger(const string &s1, const string &s2, string(*pf)(const string &, const string &)) 
{
    // 一些语句
}*/
int main()
{
    cout << "请输入两个字符串:\n";
    string s1, s2;
    cin >> s1 >> s2;

    useBigger(s1, s2, lengthCompare); //我们可以直接把函数作为实参使用,此时它会自动转换成指针
                                   //自动将函数lengthCompare 转换成指向该函数的指针
    system("pause");
    return 0; 
}

● 直接使用函数指针类型显得 很长而且繁琐。 类型别名能让我们简化使用了函数指针的代码:

string lengthCompare(const string &s1, const string &s2)
{
    return s1.size() < s2.size() ? s2 : s1;
}

// Func 和 Func2 是函数类型
typedef string Func(const string &s1, const string &s2);
typedef decltype(lengthCompare) Func2;  //等价的类型

// FuncP 和  Funcp2 是指向函数的指针
typedef string (*FuncP)(const string &, const string &);
typedef decltype(lengthCompare) *Funcp2;//等价的类型

//useBigger 的等价说明,其中使用了类型别名
void useBigger(const string &, const string &,Func);
void useBigger(const string &, const string &,FuncP2);

注意 : decltype 返回函数类型, 此时不会将函数类型自动转换成指针类型。因为decltype的结果是函数类型, 所以只有在结果前面加上 * 才能得到指针。


返回指向函数的指针 (指针函数 223P)


 

你可能感兴趣的:(Primer,2,C++,Primer,中文版(第五版))