编程语言
首页 > 编程语言> > 《C++ Primer》【Chapter 6】

《C++ Primer》【Chapter 6】

作者:互联网

Chapter6 函数

6.1 函数基础

一个函数包括:

函数的调用

通过调用符号来执行函数。调用符号是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是用逗号隔开的实参列表,用这些实参初始化函数的形参。调用表达式类型就是函数的返回类型。

函数调用主要完成两个工作:

  1. 用实参初始化函数对应的形参;
  2. 将控制权转移给被调用的函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。

return 语句可能不只是单纯的返回值

return语句在函数中有两项工作:

  1. 返回需要返回的值
  2. 将控制权从被调函数移回主调函数

形参和实参应当满足

  1. 数量对应(形参多少,实参也应该有多少)
  2. 位置对应(每个实参对应的形参位置要正确)
  3. 类型对应(类型要么一样,要么能隐式转)

局部变量和作用域

函数定义了一个语句块,语句块构成的作用域中定义的变量和形参都是局部变量。他们对于函数而言是局部的,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。(即局部变量和外层作用域同名的变量的修改互相不干扰,前提是局部有定义!而不是直接修改)

局部静态对象

这个东西还是很有意思的,需要注意的是,静态的是函数的作用域,虽然函数执行完了,但在函数作用域内,该对象只会初始化一次。

void fun() {
    static int a = 0;
    a++;
    cout << a << endl;
}

int main() {
    for(int i = 0; i < 10; ++i) {
        fun();							//输出1-10
        cout << a << endl;	//会报错,因为函数中a的静态只针对函数作用域,而不是全局的
    }
    return 0;
}

为什么函数声明和定义要分开?

一般在头文件中声明函数,在源文件中定义函数。

  1. 如果声明和定义在一个文件里,很繁琐,且容易出错;
  2. 把函数声明放在头文件中,能确保同一函数的所有声明保持一致;
  3. 一旦我们想改变函数的接口,只需要改变一条声明即可。

关于分离式编译自己的理解

分离式编译,即当项目复杂时,由于很多时候实现和声明是分离的,有的cpp文件(假设为a.cpp)只引用了头文件中的函数,并没有实现,为了不重复定义函数,可以通过编译定义函数的另一个cpp文件(b.cpp),然后通过编译器链接在一起进行编译,可以让函数实现和使用尽可能分离,独立编译,提高工作效率。

g++ -c a.cpp #编译a.cpp	得到a.o
g++ -c b.cpp #编译b.cpp	得到b.o
g++ a.cpp b.cpp -o main	#链接两个源文件形成可执行文件

6.2 参数传递

参数传递分为:引用传递值传递

传值参数

对于值传递,形参和实参是独立的,即函数中形参的修改并不会影响实参。但并不是一定不能修改函数外的值。

指针形参

对于指针而言,形参对于实参指针进行了copy,但是指针指向的对象都是一样的,当函数内对指针指向的对象操作了,指针实参指向的对象依然会改变。但是如果函数内对指针进行了修改,即指向别的对象后,指针实参的指向依然不会受到影响。

在写C++的时候不建议使用指针形参,若要修改函数外部对象,可以直接使用引用

void swap(int *a, int *b) {
  //由于指针的指向不会改变,所以为了交换函数外部两个变量的值,必须修改指针指向的对象,而不是修改指针
	int c = *a;	
	*a = *b;
	*b = c;
}

int main() {
	int a = 1, b = 2;
	std::cout << a << ", " << b << std::endl;
	swap(&a, &b);
	std::cout << a << ", " << b << std::endl;
}

传引用参数

用法和定义引用差不多,简单理解就是定义一个引用形参和实参绑定。

使用引用参数的优点:

  1. 避免拷贝浪费时间、空间
  2. 为函数返回多个结果提供有效的途径,直接通过参数返回

什么情况下用传引用的参数呢?

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

const形参和实参

回顾:顶层const表示指针本身是一个常量(作用于对象本身);底层const表示指针所指的对象是一个常量

void fun(const int i);
void fun(int i);	//错误,因为两个函数虽然形式上有差异,但实际上两者的形参没有什么不同

const形参的使用和const本身的应用是差不多的。而常量形参可以接受非常量的实参,但反之不可以。这也是为什么尽量使用常量引用的原因,可以增加参数所能接受实参的类型。

为什么要用常量引用?

  1. 增加形参数据接受范围,因为常量形参可以接受非常量的实参,但反之不可以。
  2. 对于不需要修改的数据,常量引用可以保护数据不被修改,同时减少形参实参复制的时间和空间开销。

数组形参

数组有两个性质对于使用数组参数有影响:

以下3个函数定义的方式都正确

void print(const int*);
void print(const int[]);
void print(const int[10]);	//10这个长度其实并没有影响,最后都是指针类型

单纯的指针并没有包含数组的长度,所以调用者还需要提供指针的长度信息。

管理指针形参的三种常用的技术

  1. 使用标记指定数组的长度;(例如字符串的'\0'符号)
void print(const char *cp) {
		if (cp)
      while(*cp != '\0') {
        	std::cout<<*cp++;
      }
}

这种技术适用于有明显标记的数据。

  1. 使用标准库规范

传递指向数组首元素和尾后元素的指针

void print(const int *beg, const int *end) {
	while(beg != end) {
		std::cout << *beg++;
	}
}
  1. 显示传递一个表示数组大小的形参
void print(const int *a, size_t size) {
	for(int i = 0; i < size; ++i) {
		std::cout << a[i];
	}
}

数组引用形参

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

void print(int (&arr)[10]) {
	arr[0] = 333;
}

需要注意的是,形参如果是数组的引用,那么维度也是类型的一部分,即实参维度要和形参维度对应。

传递多维数组

C++中没有真正的多维数组,所以传递多维数组实际传递的就是指针,并且传递数组第二维之后的大小(长度)都是数组类型的一部分,不能省略。

void print(const int (*mat)[3], int row_size) {
	for(int i = 0; i < row_size; ++i) {
		for(int j = 0; j < 3; ++j) {
			std::cout<<mat[i][j]<<" ";
		}
		std::cout<<std::endl;
	}
}

main:处理命令行选项

我们平常写代码定义的main函数都只有空形参列表,但偶尔也需要给main函数传递实参,一种常见情况是用户通过设置一组选项来确定函数所要执行的操作。

//main.cpp文件
int main(int argc, char *argv[]) {	//argc是传递的字符串数目,argv是传递的字符串
  for(int i = 0; i < argc; ++i) {
    std::cout<<argv[i]<<std::endl;
  }
}
./main -d -o ofile data0
输出结果是:
./main     #argv[0] 允许程序的名字
-d				 #argv[1]
-o
ofile
data0      #argv[4]

6.3 返回类型和return语句

无返回值函数

void

有返回值函数

需要注意的是,当在for循环里有return语句时,需要在for语句之后再添加一个return语句,以防止没有进入for循环。

对于返回值,一般返回的值用于初始化调用点的一个临时变量,该临时变量是函数调用的结果,即返回的是拷贝的值(如果返回的是引用,则不会真正拷贝)

不要返回局部对象的引用或指针

原因:因为函数完成时,所占用的存储空间也随之被释放掉,所以千万不能返回局部对象的引用或指针。函数终止后,局部变量的引用将指向不再有效的区域。

返回类类型的函数和调用符

即函数返回类后,可直接通过点运算符得到类里的属性

auto sz = shortString(s1, s2).size();

引用返回左值

左值和右值的区别很显然,就是能够出现在=号左边的就是左值,能出现等号右边的就是右值。

函数返回引用作为左值,把这个函数的值当成一个变量即可,但要注意的是,如果返回的是常量引用则不能作为左值。

char &get_value(std::string &str, int id) {
	return str[id];
}

int main() {
	std::string s = "Hello world!";
	std::cout << s << std::endl;
	get_value(s, 1) = 'E';
	std::cout << s << std::endl;

}

列表初始化返回值

C++11规定,函数可以返回花括号包围的值的列表。

vector<string> process() {
  return {"functionX", expected, actual};
}

main的返回值

一般函数的规定是:如果函数的返回类型不是void,那么它必须返回一个值。但是这条规定对main例外。main函数的结尾处如果没有return语句,编译器将隐式地插入一条返回0的return语句,表示执行成功,非0值根据机器不同表达含义不同。

递归

定义:函数调用自己本身。main函数不能调用自己。

返回数组指针

因为数组不能拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。

int a[] = {0,1,2};
int b = a;	//错误的,数组不能拷贝
int (*ptr)[3] = &a;	//正确	ptr是指向包含有3个整数的数组的指针

定义一个数组指针或引用

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

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

声明数组的时候,声明数组的维度是必须的

int arr[10];	//arr是一个含有10个整数的数组
int *p1[10];	//p1是一个含有10个整型指针的数组
int (*p2)[10] = &arr;	//p2是一个指向有10个整数的数组的指针

返回数组指针的函数形式:

Type (*function (parameter_list)) [dimension]

例如:

func(int i);	//表示调用func函数时需要一个int参数
(*func(int i));	//意味着我们可以对函数调用的结果执行解引用操作,即可以通过*得到指针指向的内容
(*func(int i))[10];	//表示解引用func的调用将得到一个大小是10的数组
int (*func(int i)) [10];//表示数组中的内容是int

使用尾置返回类型

任何函数的定义都能使用尾置返回,主要用于比较复杂的函数。尾置返回类型跟在形参列表后面并以->符号开头,以表示函数真正的返回类型。且一般在声明的时候,在函数最前面加一个auto

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

更粗暴的方法是直接使用decltype

int even[] = {2,4,6,8};
int odd[] = {1,3,5,7};
decltype(odd) *arrPtr(int i) {
  return (i%2)?&odd:&even;
}

6.4 函数重载

确定调用的函数应该根据函数名和参数(形参数量,形参类别)来确定。

const形参

如果形参是某种类型的指针或引用,则通过区分其指向(底层)的是常量对象还是非常量对象可以实现函数的重载。但若实参是非常量的,优先考虑非常量的版本的函数。

重载函数的标注是要看重载后是否会让函数更容易理解

const_cast和重载

const string &shortstring(const string &s1, const string &s2) {
  return s1.size()<s2.size()?s1:s2;
}
//转换为非常量的
string &shortstring(string &s1, string &s2) {
  auto &r = shortstring(const_cast<const string&>(s1), const_cast<const string&>(s2));
  return const_cast<string&>(r);
}

调用重载的函数

函数匹配:指把函数调用与一组重载函数中的某一个关联起来的过程,也叫做重载确定。通常,根据参数的个数和类型很容易确定,但是当类型是可以相互转换的类型时就比较困难。

当重载函数时有三种可能的结果:

重载与作用域

void print(const string &);
void print(double);
void foobar() {
	void print(int);	//函数内的局部作用域上声明的函数
  print("Hello world");	//报错
}

在foobar函数内声明的print(int)隐藏了函数外(外层作用域)中的同名实体

6.5 特殊用途语言特性

默认实参

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

int a(int x, int y);
int a(int x, int y);	//ok
int a(int x=2, int y);	//错误,默认参数右边的参数应该都要有默认值
int a(int x, int y=3);
int a(int x, int y=4);	//错误,该作用域内,一个形参只能被赋予一次默认形参

下面的重载函数,声明可以过,但是调用会有二义性,因为不知道该匹配哪一个。

void f(int a) {
	std::cout << "f1 : " << a << std::endl;
}

void f(int a = 1, int b = 4) {
	std::cout << "f1 : " << a << " and b " << b << std::endl;
}

int main() {
  f(1);	//错误,二义性
}

内联函数和constexpr函数

内联函数引用原因是:调用函数一般比求等价表达式的值要慢一些。当函数被频繁调用时,会浪费很多时间。

在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行

内联函数可避免函数调用的开销

将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开。内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器不支持递归内联递归,长度太长的函数不支持内联

constexpr函数

constexpr函数是指能用于常量表达式的函数。constexpr函数会被隐式的指定为内联函数。

有以下几项约定:

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

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

调试帮助

assert预处理宏

assert(expr);

对expr求值,如果表达式为假,assert输出信息并终止程序的执行。如果为真,则什么也不做。

NDEBUG预处理变量

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

可以使用一个#define语句在代码中定义NDEBUG,从而关闭调试状态。同时,很多编译器提供命令行选项定义预处理变量。

g++ -D NDEBUG main.cpp -o mian

除了C++编译器定义了静态的函数名字信息,预处理器还定义了另外4个对于程序调试有用的信息。

__func__;//函数名
__FILE__;//存放文件名的字符串字面值
__LINE__;//存放当前行号的整型字面值
__TIME__;//存放文件编译时间的字符串字面值
__DATE__;//存放文件编译日期的字符串字面值

6.6 函数匹配

重载函数的匹配对于函数的使用非常重要。

  1. 确定候选函数和可行函数:第一步就是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数一是与被调用的函数同名,二是其声明在调用点可见。

  2. 考察函数调用提供的实参:从候选函数中选出能被这组实参调用的函数,这些新选出来的函数称为可行函数。可行函数一是其形参数量与实参数量相等,二是每个实参的类型与实参类型相同,或者是能转化成形参的类型。

  3. 寻找最佳匹配:从可行函数中选出与本次调用最匹配的函数。逐一检查函数调用提供的实参,寻找类型最匹配的那个可行函数。精准类型匹配的要比需要类型准还的匹配更好。

含有多个形参的函数匹配

编译器会依次检查每个实参以确定哪个函数是最佳匹配。如果有且仅有一个函数满足下列条件,则匹配成功,否则报二义性错误。

实参类型转换

即将实参转换为形参的类型。重点是要确保类型转换后不会出错。

函数匹配和const实参

如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。即常量类型和非常量类型会进行区分,进行精准的匹配。当用非常量对象作为形参的时候,转换为常量需要进行类型转换。

6.7 函数指针

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

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

使用函数指针

与平常使用变量不同,把函数名作为一个值使用时,函数自动地转换成指针。

pf = lengthCompare;
pf = &lengthCompare;	//与上面的表达式等价
pf = 0;
pf = nullptr;	//与上式都表示指针没有指向任何一个函数

与使用指针不同的是,我们能够直接使用指向函数的指针调用该函数,无须提前解引用指针:

bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye");	//与上面的表达式等价
bool b3 = lengthCompare("hello", "goodbye");//原函数的调用,也等价

使用函数指针调用重载函数

由于函数指针的定义需要返回类型和形参类型,那么就可以通过定义直接匹配到使用哪个重载函数。

函数指针形参

虽然不能定义函数类型的形参,但是可以定义函数类型的指针作为形参。此时,形参可以看作函数类型,虽然实际上是被当成指针使用的。

bool lengthCompare(const std::string &a, const std::string &b) {
	return a.length() > b.length();
}
void useBigger(const std::string &a, const std::string &b, bool pf(const std::string &a, const std::string &b)) ;
void useBigger(const std::string &a, const std::string &b, bool (*pf)(const std::string &a, const std::string &b)) ;	//两个定义等价
useBigger(a,b, lengthCompare);	//以下两种用法都是对的
useBigger(a,b, &lengthCompare);

当函数指针或者函数类型作为形参时,传递函数的实参会自动将函数转换为指针使用

但是当使用类型别名的时候,函数类型和函数指针类型是有区分的,即不会将函数类型自动转换为指针。

typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;	//与上面的等价
typedef bool (*FuncP)(const string&, const string&);	//这里可能不是很好理解,但是类别typedef long long ll;感觉就是将表达式(long long ll = 333;)前面加个typedef就是起类型别名了。
typedef decltype(lengthCompare) *FuncP2;//与上面的等价

返回指向函数的指针

首先,铭记不能返回函数,那么用类型别名可以方便返回函数指针。如果不好理解类型别名,可以用尾置返回类型的方式。

auto f(int) -> int (*) (int*, int);	//f函数的返回类型是int (int*, int)类型函数的指针

将auto和decltype用于函数指针类型

decltype一般用于明确知道返回类型是什么类型的函数指针。

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
decltype(sumLength) *getFcn(const string &);	//声明一个函数,返回类型是sumLength函数类型的指针,注意不是(*getFcn),所以getFcn是函数而不是指针!

需要注意的是,decltype作用于函数时,它返回函数类型而非指针类型!

标签:Chapter,const,函数,形参,int,C++,实参,Primer,指针
来源: https://www.cnblogs.com/dybala21/p/16525454.html