ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

《C++ Primer》【Chapter 6】

2022-07-27 17:02:59  阅读:237  来源: 互联网

标签:Chapter const 函数 形参 int C++ 实参 Primer 指针


Chapter6 函数

6.1 函数基础

一个函数包括:

  • 返回类型
  • 函数名字
  • 0个或多个形参组成的列表
  • 函数体

函数的调用

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

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

  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表示指针所指的对象是一个常量

  • 当形参有顶层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);
}

调用重载的函数

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

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

  • 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
  • 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
  • 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性(ambiguous call)

重载与作用域

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函数会被隐式的指定为内联函数。

有以下几项约定:

  • 函数的返回类型及所有形参的类型都是字面值类型(算术类型1+2、引用、指针)
  • 函数体中有且仅有一条return语句
  • 允许返回值并非一个常量

内联函数和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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有