c++primer第六章:函数

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、函数基础

函数是一个命名了的代码块,通过调用运算符(小括号)来执行函数,圆括号内使用逗号隔开的实参列表,我们用实参初始化函数的形参。

#include<iostream>

using namespace std;

int fact(int ival) {
	int jval = 1;
	while (ival != 1) {
		jval *= ival--;
	}
	return jval;
}

int main() {
	int kval = fact(5);
	cout << kval << endl;
}
int jdz(int ival) {
	int jval = abs(ival);
	return jval;
}

int main() {
	cout << jdz(-5) << endl;
}

静态局部对象:

第一次对象定义时初始化,知道程序结束才被销毁。局部静态对象默认值初始化为0.

#include<iostream>

using namespace std;

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

int main() {
	for (int i = 0; i != 10; ++i)
		cout << count_calls() << endl;
}

函数声明

函数声明与定义区别在于无函数体,包括三要素(返回类型,函数名,形参类型),可以确定函数接口。通常将函数声明放在头文件中以确保接口一致

int fact(int ival);

分离式编译

.cc生成.o文件(表示包含对象代码),然后由编译器将各个.o文件link在一起生成可执行文件。

二、参数传递

每次函数调用时都会重新创建它的形参,并用实参进行初始化。
与其它变量相同,形参类型决定了形参和实参交互的方式。如果形参是引用类型,他将绑定到实参上(passed by reference);否则,将实参的值拷贝后赋给形参(passed by value)。

传值参数

指针形参拷贝的是指针的值,此时可以间接修改指针指向的对象。

void reset(int *p)
{
*p = 0;
p=0;
}
int i = 42;
reset(&i);
cout << "i= "  << i << endl;  //输出i = 0;
#include<iostream>

using namespace std;

void sw(int * p1, int* p2) {
	int ival = *p1;
	*p1 = *p2;
	*p2 = ival;
}

int main() {
	int a = 0, b = 1;
	sw(&a, &b);
	cout << a << b << endl;
}

传引用参数

想要避免拷贝又不想改变对象内容是可以使用常量引用。(注:常量引用即可以用于被引用对象为常量,也可以用于被引对象为非常量)。
使用引用形参可以方便返回额外信息。

#include<iostream>

using namespace std;

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::size_type occ;
	char c = 'e';
	string::size_type ret = find_char("Werner", c, occ);
	cout << "��һ�γ���" << c << "��λ��Ϊ��"<<ret << endl
		<< c << "���ܳ��ִ���Ϊ��"<< occ << "��" << endl;
}

const形参和实参

与初始化的内容相同,注意不能用底层const初始化非const对象。尽量使用常量引用,因为形参为非常量只能接受非底层const实参,而常量引用则无此限制。

数组形参

数组有两个特殊性质:一、不允许拷贝数组。二、使用数组时通常会将其转化为指针。
以下三个函数完全等价的。

void print(const int*);
void print(const int[]);
void print(const int[10]);   //代表我们期望有多少元素,实际不一定。
int i = 0,j[2] = {0,1};
print(&i);
print(j);     //j转化成int*并指向j[0]

C++允许将变量定义成数组的引用,所以形参也可以是数组的引用。

void print(int (&arr)[10])
{
	for(auto elem: arr)
	cout << elem << endl;
}
练习
#include<iostream>

using namespace std;
/*
int fuc(int a, int* b) {
	if (a > *b)
		return a;
	else
		return *b;
}

int main() {
	int a = 1;
	int b = 0;
	cout << fuc(a, &b) << endl;
}
*/
void fuc2(int** a, int** b) {
	int* c = *a;
	*a = *b;
	*b = c;
}

int main() {
	int a = 0, b = 1;
	int* p1 = &a, * p2 = &b;
	fuc2(&p1, &p2);
	cout << *p1 << *p2;
}
#include<iostream>

using namespace std;

void print1(const int cp[]) {
	cout << *cp << endl;
}
void print2(const int* beg,const int* end) {
		while (beg != end)
		{
			cout << *beg++ << " ";
		}
		cout << endl;
}
int main() {
	int i = 0, j[2] = { 0, 1 };
	print1(&i);
	print2(begin(j),end(j));
}
#include<iostream>

using namespace std;

void print(const int ia[10]) {
	for (size_t i = 0; i != 10; ++i)
		cout << ia[i] << endl;
}

int main() {
	int i[10] = {};
	print(i); 
}
main

C++标准允许主函数main()有或没有参数列表。 能在主函数main()中使用一个或更多的参数。 如下是一些惯例:

int main(int argc, char *argv[])

第一个参数argc,指明有多少个参数将被传递给主函数main(),真正的参数以字符串数组(即第2个参数argv[])的形式来传递。每一个字符串均有自己意思。在这句法上面, 记住, argc代表参数的数量, main()函数本身是在索引0为的第一参数。 所以, argc总是至少为1 。它的总数是从argv列阵的元素数目。 这意味着, argv[0]的值是至关重要的。 如果用户在控制台环境中程序名称后键入含参数的指令, 那么随后的参数将传递给argv[1]。,第一个用于输入可执行文件名:

 prog -d -o ofile data0

上面命令所提供的argv如下:
argv[0] = “prog”; //也可以指向一个空字符串
argv[1] = “-d”;
argv[2] = “-o”;
argv[3] = “ofile”;
argv[4] = “data0”;
data[5] = 0; //最后一个元素固定为0

练习
#include<iostream>
using namespace std;

void print(const char* c) {
	string str;
	while (*c)
	{
		str += *c++;
	}
	cout << str;
}

int main(int argc, char *argv[]) {
	for(int i = 1;i < argc;i++)
	print(argv[i]);
}

终端输入.\625.exe hellow world
在这里插入图片描述

含有可变形参的函数

有两种方法:1、initializer_list2、可变参数模板,见16章
initializer_list形参:数量未知但是全部实参的类型相同,与vector不同,initializer_list中的所有元素均为常量值。
向initializer_list中传递值序列,需要将他们放在一个花括号之内。

 initializer_list<string> ls;
练习
#include<initializer_list>
#include<iostream>

using namespace std;

int sum(initializer_list<int> in)
{
    int sum(0);
    for(const auto &i:in){
        sum+=i;
    }
    return sum;
}

int main()
{
    int i(0);
    i = sum({0,1,2,3,4,5});
    cout<< i << endl; 
}
省略符形参

C++程序访问C代码而设置。
void foo(parm_list,…) ;
void foo(…);

三、返回类型和return语句

return作用:终止当前正在执行的函数并将控制权返回调用函数的地方。
返回一个值的方式和初始化一个变量或形参的方式完全一样:函数的返回值用于初始化在调用函数处创建的临时对象。在求解表达式时,如果需要一个地方储存其运算结果,编译器会创建一个没有命名的对象,这就是临时对象。
与初始化一样,当返回值是引用类型时,调用函数和返回结果时返回的时候没有进行复制,返回的仅仅是所引对象的别名。注意不要返回局部对象的引用或指针,当函数执行完后,局部变量的存储空间会被释放,对局部对象的引用就会指向不确定的内存。
引用返回的是左值,因此可以进行修改。

练习
#include<iostream>
#include<vector>

using namespace std;

void print(const vector<int> &v,decltype(v.begin()) iter)  ///注意形参一定要加引用,否者形参初始化的拷贝操作会让迭代器失效。
{    

    if (iter != v.end())
    {
        cout << *iter++ << " "; 
        return print(v,iter);
    }
    return;
}

int main()
{
    vector<int> v{0,1,2,3,4,5,6,7,8,9,10};
    print(v,v.begin());
}

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组,但是可以返回数组的指针:

int(*fun(int i))[10];

使用类型别名可以简化这一任务:

typedef int arrT[10];     //类型别名,含有10个int的数组
using arrT = int[10];     //等价声明
arrT* func(int i);

或者使用尾置返回类型:

auto func(int i) -> int(*) [10];

使用decltype

int odd[] = {1,3,5,7,9};
decltype(odd) *arrPtr(int i)
{...}
练习
string (&fun())[10]{};
using arrS = string[10];
arrS (&fun()){};
auto fun() -> string (&) [10];
decltype string arrS[10];
arrS (&fun()){};
#include<iostream>

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()
{
    decltype(odd) *arr;
    arr= &arrPtr(6);
}

四、函数重载

1、同一个作用域内的几个函数名字相同但形参列表不同。
2、有无顶层const无法区分两个函数形参列表的不同,但是有无底层const可以区分不同函数形参。
3、const_cast与重载,如以下例子,参数和返回类型都是const string的引用,对非const使用时返回的仍然是const,不满足要求:

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

所以需要一个非const string版本的函数:

string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&> (s1), const_cast<const string&> (s2));
return const_cast<string&> (r);
}

4、如果在内层作用域内声明函数,它将隐藏外层作用域中声明的同名实体,即在不同的作用域中无法重载函数名。

五、特殊用途语言特性

三种函数相关的语言特性:默认实参、内联函数和constexpr函数。

默认实参

可以为函数指定默认的形参:

typedef string::size_type sz;
string screen(sz ht = 24,sz wid = 80, char background = " ");

注意一旦一个形参被赋予了默认值,它后面所有的形参都必须有默认值。
使用默认实参只需要调用的时候忽略就可以了,但需要注意只能忽略尾部的实参,同时形参可以相互转化的情况:

window = screen(, , "?");   //错误,只能忽略尾部的实参
window = screen("?");       //调用混乱,调用screen('?',80,'' )

第二个例子将?转化成将要sz的类型,与调用的意图相悖。
所以设计时尽量将不怎么使用默认值的形参放前面。

默认实参声明

函数声明通常放在头文件中,一个函数通常只声明一次。声明多次时,注意默认实参只能添加一次:
string screen(sz ,sz, char = " "); //表示只有第三个有默认值
string screen(sz ,sz, char = " "); //错误:重复声明
string screen(sz=24 ,sz=80, char);//正确:添加默认实参

练习
#include<iostream>
#include<string>

using namespace std;

string make_plural(size_t ctr, const string &word = "s", 
                                const string &ending = "s")
{
    return(ctr > 1) ? word + ending : word;
}

int main()
{
    cout << make_plural(2) << endl;
}
内联函数和constexpr函数

内联函数:
内联函数可避免函数调用的开销,在返回类型前加上关键字inline即可,一般用于小型、频繁调用的函数。
问题:内联能提高函数的执行效率,为什么不把所有的函数都定义成C++内联函数?
答:如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
constexpr函数:
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显示,字面值属于常量表达式,用表达式初始化的const对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:

const int max_num = 20;           // max_num是常量表达式
const int limit = max_num + 1;    // limit 是常量表达式
int staff_size = 2;               // staff_size 不是常量表达式,因为staff_size没有用const修饰
const int sz = get_size();        // sz 不是常量表达式,虽然sz是个常量,但它的值在运行时才能确定

注意:constexpr函数可以触发编译时求值只是代表有这个能力,不一定返回常量表达式,需要同时满足:
1、所有参数、返回值都是常量表达式
2、返回的结果被用于常量表达式
才会触发编译时求值。同时该函数语句只允许一条return或者运行时不执行任何操作的语句(类型别名、using声明等)
constexpr指能用于常量表达式(见2.4.4节)的函数,需要注意该函数的形参与返回类型必须是字面值类型,且必须有且只有一条return语句。constexpr必须用常量表达式初始化。constexpr函数被隐式的指定为内联函数。
内联函数/constexpr函数通常定义在头文件中。

练习
#include<iostream>

using namespace std;

inline bool isShorter(const string& s1, const string& s2) {
	return s1.size() < s2.size();  
}

int main() {
	const string s1 = "WERN", s2 = "ER";
	cout << isShorter(s1, s2);
}
#include<iostream>

using namespace std;

constexpr bool isShorter(const string& s1, const string& s2) {
	return s1.size() < s2.size();  //错误,const返回值要求常量表达式,它必须在编译时确定值得大小
}

int main() {
	const string s1 = "WERN", s2 = "ER";
	cout << isShorter(s1, s2);
}
调试帮助

程序包含一些只在开发时使用的调试代码时,可以使用两项预处理功能:assert和NDEBUG.
assert预处理宏:
标准形式:
assert(expr);
expr值为真,什么也不做。为假,输出信息并终止程序执行。
NDEBUG预处理变量
如果定义了NDEBUG预处理变量,则assert什么也不做。
此外,直接使用NDEBUG编写自己的条件检查代码。

#ifdef NDEBUG
...;
#endif

预处理器中几个对于程序调试有用的名字:

__FILE__    存放文件名的字符串字面值
__LINE__   存放当前行号的整形字面值
__TINE__   存放文件编译时间的字符串字面值
__DATE__   存放当前日期的字符串字面值

注意宏定义NDEBUG要在#include语句之前出现

#define NDEBUG

#include<iostream>
#include<vector>
#include<cassert>


using namespace std;

void print(const vector<int> &v,decltype(v.begin()) iter){
    if (iter != v.end())
    {
        cout << *iter++ << " "; 
        cout << v.size() << " "; 
        return print(v,iter);
    }
    return;
}

int main()
{
    vector<int> v{0,1,2,3,4,5,6,7,8,9,10};
    assert(v.size() < 5);
    print(v,v.begin());
}

六、函数匹配

练习
#include<iostream>

using namespace std;

void f() {};
void f(int i) {cout << i << endl;};
void f(int i, int j) {cout << i << " " << j << endl;};
void f(double i, double j= 3.14) {cout << i << " " << j << endl;};

int main() {
	//f(2.56, 42);  //错误,二义性调用
	f(42);
	f(42, 0);
	f(2.56, 3.14);
}

六、函数指针

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

bool lengthCompare(cconst string&, const string &);

该函数的类型是bool (cconst string&, const string &).
函数指针的声明只需要将函数名替换:

bool (*pf)(cconst string&, const string &);

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

//这两句代码等效
pf = lengthCompare;
pf = &lengthCompare;

此外,还可以直接使用函数指针,无需解引用。

//这几句等效
bool b1 = pf("Hellow","Goodbye");
bool b1 = (*pf)("Hellow","Goodbye");
bool b1 = lengthCompare("Hellow","Goodbye");

函数指针可以作为形参使用,此时函数名可以自动转化为函数指针;可以使用typedef避免冗长。
同样,返回类型可以为函数指针,但是不能使用函数名,因为不会自动转换。

int(*f1(int))(int*,int);

该f1右边括号表示是个函数,左边*表示返回指针,外层右边括号表示指针指向函数,外层左边int表示返回int。
可以使用using避免冗长:

using F = int (int*,int);
using PF = int (*)(int*,int);
PF f1(int);
F *f1(int);

还可以使用尾置类型:

auto f1(int) ->int (*)(int*,int);
练习
#include<iostream>
#include<vector>

using namespace std;

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return abs(a - b); }
int multi(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int Func(int a, int b);  //vector<int (*)(int, int)> vec;

int main() {
	vector<decltype(&Func)> vec;
	
	vec.push_back(add);
	vec.push_back(subtract);
	vec.push_back(multi);
	vec.push_back(divide);
	
	int a = 10, b = 1;

	for (auto& i : vec)
		cout << (*i)(a, b) << endl;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值