C++ Primer Chapter 6 Functions

C++ Primer Chapter 06 Functions

注:CPP Primer这本书从第六章开始学习,学习过程中涉及之前章节的内容,将会跳至相应章节学习。
2024/05/28

第6章 函数

6.1 函数基础

函数的形参列表
形参名是可选的。
函数返回类型
函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

6.1.1 局部对象
局部静态对象
local statice object
某些时候,有必要令局部变量的声明周期贯穿函数调用及之后的时间。可以将局部变量定义为static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止时才被销毁,在此期间即使对象所在的函数结束执行也不会对他有影响。
//惊了,发现C#也有这个特性,狗头

#include <iostream>

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

int main() {
	//int a;
	//std::cout << a << std::endl;

	//Error	C4700	uninitialized local variable 'a' used	 
	for (size_t i = 0; i < 10; i++)
	{
		std::cout << count_calls() << std::endl;
	}
	getchar();

}

6.1.2 函数声明

含有函数声明的头文件应该被包含在定义函数的源文件中

6.1.3 分离式编译

编译和链接多个源文件

//Ch06_Header.h
#pragma once
#include <stdlib.h>
#include <stdio.h>
int frac(int);
 //factorial.cpp
#include "Ch06_Header.h"
int frac(int p_val) {
	int ret = 1;
	while (p_val > 1)
	{
		ret *= p_val;
		p_val--;
	}
	return ret;
}


//factorialMain.cpp
#include "Ch06_Header.h"

int main() {
	int i = 6;
	int j = frac(i);
	//字符串插值符号是%
	printf("%d! is %d\n", i, j);
	system("pause");
	return 0;
}

命令行编译指令

********************************************************************
** Visual Studio 2022 Developer PowerShell v17.3.5
** Copyright (c) 2022 Microsoft Corporation
PS H:\C_CPP_CUDA\LearningCPP\CPP_Primer_5th> cl factorial.cpp
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.33.31630 版
版权所有(C) Microsoft Corporation。保留所有权利。

factorial.cpp
Microsoft (R) Incremental Linker Version 14.33.31630.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:factorial.exe
factorial.obj
LINK : fatal error LNK1561: 必须定义入口点
PS H:\C_CPP_CUDA\LearningCPP\CPP_Primer_5th> cl factorialMain.cpp
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.33.31630 版
版权所有(C) Microsoft Corporation。保留所有权利。

factorialMain.cpp
Microsoft (R) Incremental Linker Version 14.33.31630.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:factorialMain.exe
factorialMain.obj
factorialMain.obj : error LNK2019: 无法解析的外部符号 "int __cdecl frac(int)" (?frac@@YAHH@Z),函数 _main 中引用了该符号
factorialMain.exe : fatal error LNK1120: 1 个无法解析的外部命令
PS H:\C_CPP_CUDA\LearningCPP\CPP_Primer_5th> cl factorialMain.obj factorial.obj
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.33.31630 版
版权所有(C) Microsoft Corporation。保留所有权利。

Microsoft (R) Incremental Linker Version 14.33.31630.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:factorialMain.exe
factorialMain.obj
factorial.obj
PS H:\C_CPP_CUDA\LearningCPP\CPP_Primer_5th>

如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件。

6.2 参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

Note 形参初始化的机理与变量初始化一样。

6.2.1 传值参数

指针形参
指针的行为和其他非引用类型一样了。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值。

void reset(int *ip)
{
	*ip=0;		//改变指针ip所指对象的值
	ip=0;		//只改变了ip的局部拷贝,实参未被改变
}

Best Practice 熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。

6.2.2 传引用参数

使用引用避免拷贝

Best Practices 如果函数无须改变引用形参的值,最好将其声明为常量引用。

void reset(int &i)	//i是传给reset函数的对象的另一个名字
{
	i=0;			//改变了i所引对象的值
}

使用引用形参返回额外信息

6.2.3 const形参和实参

和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的:

void fcn(const int i){	/* fcn能够读取i,但是不能向i写值*/	}

调用fcn函数时,即可以传入const int也可以传入int。忽略掉形参的顶层const可能产生意想不到的结果:

void fcn(const int i){	/* fcn能够读取i,但是不能向i写值*/	}
void fcn(int i){	/* ... */	}	//错误:重复定义了fcn(int)

指针或引用形参与const

int i=42;
const int *cp=&i;	//正确:但是cp不能改变i
const int &r=i;		//正确:但是r不能改变i
const int &r2=42	//正确:
int *p=cp;			//错误:p的类型和cp的类型不匹配
int &r3=r;			//错误:r3的类型和r的类型不匹配
int &r4=42;			//错误:不能用字面值初始化一个非常量引用 

将同样的初始化规则应用到参数传递上可得到如下形式:

int i=0;
const int ci=i;
string::size_type ctr=0;
reset(&i);		//调用形参类型时int*的reset参数
reset(&ci);		//错误:不能用指向const int对象的指针初始化int*
reset(i);		//调用形参参数是int&的reset函数
reset(ci);		//错误:不能把普通引用绑定到const对象ci上
reset(42);		//错误:不能把普通引用绑定到字面值上
reset(ctr);		//错误:类型不匹配,ctr是无符号类型
//正确:find_char的第一个形参是对常量的引用
find_char("Hello World",'o',ctr);

尽量使用常量引用
把函数不会改变的参数定义成(普通的)引用时一种比较常见的错误,这么做带给函数的调用者一种误导,即函数可以改变它的实参的值。
此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。
就像刚刚看到的,我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

//不良设计:第一个形参的类型应该是const sting&
string::size_type find_char(string &s,char c,string::size_type &occurs);

6.2.4 数组形参

管理指针形参:
使用标准库规范

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

3.5.3 指针和数组

标准库函数begin和end
C++11新标准引入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:

int ia []={0,1,2,3,4,5,6,7,8,9};		//ia是一个含有10个整数的数组
int *beg=begin(ia);						//指向ia首元素的指针
int *last=end(ia);						//指向arr尾元素的下一个位置的指针

Note 一个指针如果指向了某种内置类型数组的尾元素的“下一位置”,则其具备与vector的end函数返回的与迭代器类似的功能。特别要注意,尾后指针不能执行解引用和递增操作。

**数组引用形参**
void print(int (&arr)[10]){
	for(auto elem:arr)
		cout<<elem<<endl;
}

Note &arr两端的括号必不可少

f(int &arr [10]) 		//错误:将arr声明成了引用的数组
f(int (&arr) [10])	//正确:arr是具有10个整数的整型数组的引用
int i=0,j[2]={0,1};
int k[10]={0,1,2,3,4,5,6,7,8,9};
print(&i);			//错误:实参不是含有10个整数的数组
print(j);			//错误:实参不是含有10个整数的数组
print(k);			//正确:实参是含有10个整数的数组

传递多维数组

//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[int],int rowSize){ /*...*/}
//等价定义
void print(int [][10],int rowSize){ /*...*/}

Note 再一次强调,*matrix两端的括号必不可少:

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

6.2.5 main:处理命令行选项

WARNING 当时用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

6.2.6 含有可变形参的函数

为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:
如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;
如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。
intializer_list 形参
initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。
和vector一样,initializer_list也是一种模板类型。定义initializer_list对象时,必须说明列表中所含元素的类型:

initializer<string>ls;		//initializer_list的元素类型是string
initializer<int> li;		//initializer_int的元素类型是int

和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中的元素的值。

void error_msg(initializer_list<string> il)
{	
	for(auto beg=il.begin();beg!=il.end;++beg)
		cout<<*beg<<" ";
	cout<<endl;
}

如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内:

//expected和actual是string对象
if(expected!=actual)
	error_msg({"function",expected,actual});
else 
	error_msg({"function","okay"});

含有initializer_list形参的函数也可以同时拥有其他形参。

void error_msg(ErrCode e,initializer_list<string> il)
{
	cout<<e.msg()<<": ";
	for(const auto &elem:il)
		cout<<elem<<" ";
	cout<<endl;
}

6.3 返回类型和return语句

6.3.2 有返回值函数

WARNING 在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。

值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
同其他引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名。

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

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

const string &manip()
{
	string ret;
	if(!ret.empty())
		return ret;				//错误:返回局部对象的引用
	else
		return "Empty";			//错误:"Empty"是一个局部临时量

引用返回左值

 #include "Ch06_Header.h"

char& get_val(std::string& str, std::string::size_type ix) {
	return str[ix];
}
int main() {
	std::string s("a value");
	std::cout << s << std::endl;
	get_val(s, 0) = 'A';
	std::cout << s << std::endl;
}

递归

在递归函数中,一定以后某条路径是不包含递归调用的;否则,函数将“永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止。

Note main函数不能调用它自己。

6.3.3 返回数组指针

声明一个返回数组指针的函数

int main() {
	int arr[10];			//arr is an array of ten ints
	int* p1[10];			//p1 is an array of ten pointers
	int(*p2)[10] = &arr;	//p2 pointers to an array of ten ints
}

返回数组指针的函数形式如下所示:

Type (*funtion(parameter_list)) [dimension]
Type 表示元素的类型
dimension表示数组的大小
(*function(parameter_list))两端的括号必须存在。
如果没有这对括号,函数的返回类型将是只指针的数组。
int (*func(int i))[10]func(int i)表示调用func函数时需要一个int类型的实参
•(*func(int i))意味着我们可以对函数调用的结果执行解引用操作
•(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
•int (*func(int i))[10]表示数组中的元素是int类型

使用尾置返回类型
在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。
尾置返回类型跟在形参列表后面并以一个->开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:

auto func(int i)-> int(*)[10];
//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值