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个整数的数组