第六章
6.6
形参、局部变量和局部静态变量
- 形参和定义在函数体内部的变量统称为局部变量,它们仅在函数的作用域内可见
- 函数体内的局部变量可以分为普通局部变量和静态局部变量
- 形参:在函数开始时便为其申请内存空间,调用时用实参进行初始化。形参是只存在于块执行期间的对象
- 普通局部变量在所载块结束后就失效了
- 静态局部变量对应的对象称为局部静态对象,它的生命周期从定义语句开始一直到程序结束
6.8
在头文件内进行函数声明:和变量类似,在头文件声明然后在源文件定义。预处理器变量的名字全部大写,可以避免与程序中别的实体发生名字冲突。
#ifndef CHAPTER_H_INCLUDED
#define CHAPTER_H_INCLUDED
int fact(int);
double myABS(double);
#endif
以下引用自:#pragma指令与#ifndef指令区别
在C/C++中,在使用预编译指令#include的时候,为了防止重复引用造成二义性,通常有两种方式——
- 第一种是#ifndef指令防止代码块重复引用,比如说
#ifndef _CODE_BLOCK #define _CODE_BLOCK // code #endif// _CODE_BLOCK` ```
- 第二种就是#pragma once指令,在想要保护的文件开头写入
#pragma once
- #ifndef方式是C/C++语言的标准支持,也是比较常用的方式,#ifndef的方式依赖于自定义的宏名(例中的_CODE_BLOCK)不能冲突,它不光可以保证同一份文件不会被包含两次,也能够保证不同文件完全相同的内容不会被包含两次。
但,同样的,如果自定义的宏名不小心“重名”了,两份不同的文件使用同一个宏名进行#ifndef,那么会导致编译器找不到声明的情况(被编译器判定为重定义而屏蔽了)
此外,由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,#ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式(Visual Studio 2017新建头文件会自带#pragma once指令)- #pragma once一般由编译器提供保证:同一个文件不会被包含多次。这里所说的”同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。无法对一个头文件中的一段代码作#pragma once声明,而只能针对文件。
此方式不会出现宏名碰撞引发的奇怪问题,大型项目的编译速度也因此提供了一些。缺点是如果某个头文件有多份拷贝,此方法不能保证它们不被重复包含。在C/C++中,#pragma once是一个非标准但是被广泛支持的方式。
- 二者的区别:
#pragma once方式产生于#ifndef之后。#ifndef方式受C/C++语言标准的支持,不受编译器的任何限制;而#pragma once方式有些编译器不支持(较老编译器不支持,如GCC 3.4版本之前不支持#pragmaonce),兼容性不够好。#ifndef可以针对一个文件中的部分代码,而#pragma once只能针对整个文件
6.9(分离式编译)
- 分离式编译允许我们把程序分割到几个文件中,每个文件独立编译
- 举例:(C++Primer第五版 第6.1.3节)
fact函数定义在fact.cc中;fact函数声明位于Chapter.h中;fact函数在factMain.cc文件中的main函数中被调用。
这一过程通常会产生一个后缀名为.obj(Windows)或者.o(UNIX)的文件,含义是该文件包含object code。
编译器会把对象文件链接在一起形成可执行文件。 - 分离式编译时具体代码
fact.cc
#include "Chapter6.h"
using namespace std;
//定义函数fact
int fact(int val)
{
if (val < 0)
return -1;
int ret = 1;
for (inti = 1; i != val + 1; ++i)
ret *= i;
return ret;
}
factMain.cc
#include "Chapter6.h"
#include <iostream>
using namespace std;
//调用函数fact
int main()
{
int num;
cout << "请输入一个数" << endl;
cin >> num;
cout << num << "的阶乘是:" << fact(num) << endl;
return 0;
}
6.12(传值和传引用)
使用引用可以直接操作所引用的对象,而且可以避免拷贝。
void f(T): 形参采用传值的方式,实参被拷贝给形参
void f(T&):形参采用传引用的方式,实参是形参的别名
6.14(引用传递相对于值传递的优势)![在这里插入图片描述](https://img-blog.csdnimg.cn/bdf81f390f2c4e8ea0d4c834cc1d8117.png)
6.23
三个版本print函数:依次调用,输出int i = 0;
和 int j[2] = {0, 1};
的 i 和 j
- 不控制指针的边界(参数是常量整型指针)
void print1(const int *p)
{
cout << *p << endl;
}
- 调用者指定数组的维度(两个参数,分别是常量整型指针和数组的容量)
void print2(const int *p, const int sz)
{
int i = 0;
while(i != sz)
{
cout << *p++ << endl;
++i;
}
}
- 使用C++11新规定的begin和end函数限定数组边界(两个参数,分别是数组的首尾边界)
void print3(const int *b, const int *e)
{
for (auto q = b; q != e; ++q)//记得用后置运算符
{
cout << *q << endl;
}
}
- 调用如下
print1(&i1);
print1(j);
print2(&i, 1);
print2(j, sizeof(j)/sizeof(*j));
print3(begin(j), end(j));
6.24(不指定形参数组维度)
void print(const int ia[10]) { //循环输出数组的值}
形参ia的维度10此时只是我们期望的,但是实际上不一定,而且真实维度如果不是10我们依然可以正常调用print函数
修改后应该是:void print(const int ia[], const int sz) { //循环输出数组的值}
6.25(main处理命令行选项 6.2.5节)
6.27(含有可变形参的函数 6.2.6节)
一个可以计算列表中所有元素的和的函数:
#include <iostream>
using namespace std;
int iCount(initializer_list<int> il)
{
int count = 0;
for (auto i : il)
{
count += i;
}
return count;
}
int main()
{
//列表初始化initializer_list<T>
cout << "1,1,2,2,3的和是:" << iCount({ 1,1,2,2,3 }) << endl;
return 0;
}
6.31(引用何时有效)
引用所引的是函数开始之前就已经存在的对象,则返回该引用是有效的
引用的是函数的局部变量则随着函数的结束,局部变量失效,此时返回的引用无效
不希望改变返回的对象时,返回对常量的引用
6.32
int &get(int *array, int index) {return array[index];}
注意return处不要加&,否则会变成返回 array[ index ] 的地址
6.36(返回数组的引用的函数)
string (&func( )) [10];
&func( )表示函数的返回结果是一个引用
(&func( )) [10]表示引用的对象是一个维度为10的数组
string (&func( )) [10]表示数组的元素是string对象
6.37
直接编写返回数组引用的函数比较繁琐,使用类型别名,尾置返回类型和decltype关键字都可以简化
- 使用类型别名
typedef string arr[10]
//using arr = string[10];
arr& func( );
- 使用尾置返回类型
auto func( ) -> string(&) [10];
- 使用delctype关键字
string str[10];
decltype(str) &func( );
返回数组指针
- 使用类型别名
arrT是含有10个整数的数组的别名,func返回一个指向含有10个整数的数组的指针
typedef int arrT[10];
//using arrt = int[10];
arrT* func(int i);
- 声明一个返回数组指针的函数(不使用类型别名)
定义返回数组指针的函数,数组的维度必须在形参列表的后面
例子: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类型 - 使用尾置返回类型
尾置返回类型跟在形参列表的后面并且以一个 -> 符号开头,函数真正返回类型在形参列表之后,原本防止返回类型的地方放置auto
注意解引用运算符加了括号
//func节后一个int类型的实参,返回指向含有10个整数的数组的指针
auto func(int i) -> int (*) [10]
- 使用decltype
在已知函数返回的指针指向那个数组的情况下可以用decltype关键字声明返回类型
int odd[10] = {1, 3, 5, 7, 9};
decltype(odd) *arrPtr(int i);
arrPtr使用关键字decltype表示它的返回类型是一个指针,且指针指向的对象和odd类型一致。decltype并不负责把数组类型转换为对应的指针,decltype的结果是一个数组,必须在函数声明的时候加一个*符号表示arrPtr返回指针。
6.43(内联函数和constexpr函数)
内联函数和constexpr函数通常定义在头文件内,以保证多个定义完全一致,同时内联函数的定义应该对编译器来说是可见的,编译器才能在调用点内联展开函数的的代码
- 内联函数,在函数的返回类型前面加 inline ,但是该请求编译器可以忽略
- constexpr函数,能用于常量表达式的函数,constexpr函数额外规定:函数的返回类型和所有形参的类型都是字面值类型;函数体有且只有一条return语句。
constexpr int new_sz() { return 42;};
constexpr int foo = new_sz();
- constexpr函数不一定返回常量表达式,即允许constexpr函数的返回值不是常量
const xize_t scale( size_t cnt) { return new_sz() * cnt; }
scale(2)
返回常量表达式,int i = 2, scale(i)
返回的不是常量表达式
6.47(调试器 6.5.3节)
assert 预处理宏
assret(espr),表达式为假,assert输出信息并终止程序
NDEBUG
如果定义了NDEBUG,则assert什么也不做
使用 #define 语句定义NDEBUG,可以关闭调试状态
- 可以使用NDEBUG编写自己的条件调试代码,NDEBUG未定义就会执行
#ifndef
和#endif
之间的代码,NDEBUG已定义这些代码会被忽略:
此处如果打开调试器,就会执行中间的内容,关闭调试器则不会
#ifndef NDEBUG
//……………………
#endif
6.49(候选函数和可行函数 6.6节)
- 函数匹配第一步:选出调用对应的重载函数集,集合内的就是候选函数
- 函数匹配第二步:候选函数中选出可以被这组实参调用的函数,即可行函数(形参和实参数量相等,实参和形参类型相同或者可以转化)
6.52(实参类型转换 6.6.1)
实参类型到形参类型转换的等级排序:
6.54(函数指针 6.7节)
- 函数的类型由返回类型和形参类型共同决定,例如指向函数类型
bool (const string &, const string &)
的函数指针:bool (*pf)(const string &, const string &)
:
pf 前面由*表示 pf 是一个指针,右侧形参列表表示 pf 指向的是函数,左侧的是函数的返回类型
如果没有括号变成bool *pf(const string &, const string &)
,pf 就是一个返回bool指针的函数 - 如果把函数名作为一个值使用,它就会自动地转换成指针
bool lengthCompare(const string &, const string &)
pf = lengthCompare;//取地址符是可选的
//pf = &lengthCompare
- 直接用指针调用函数,不需要提前解引用,以下三个调用等价
bool b1 = pf("sadad", "dsada");
bool b1 = (*pf)("sadad", "dsada");
bool b1 = lengthCompare("sadad", "dsada");
- 可以为函数指针赋值nullptr或0,表示不指向任何一个函数:
pf = nullptr
- 重载函数指针举例
void ff(int*);
void ff(unsigned int);
void (*pf)(unsigned int) = ff//pf指向ff(unsigned int)
- 函数指针可以作为形参使用,形参虽然看起来是函数类型但是实际上被当成指针使用
void f(bool pf(const string &, const string &));
//void f(bool (*pf)(const string &, const string &))
f(lengthCompare);//直接把函数作为实参,其会自动转换成指针
简化函数指针(类型别名 或者 decltype)
重点注意:
①函数类型,但是在作为形参的化会自动转换为指针
②decltype返回的函数类型不会自动转化成指针类型,需要在结果上加上*
③和形参不同,函数返回类型不会自动转换成指针
- 此处的func1只是函数类型,但是在作为形参的化会自动转换为指针
//func是函数类型
typedef decltype(lengthCompare) func1;
//等价
typedef bool func1(const string &, const string &));
- decltype返回的函数类型不会自动转化成指针类型,需要在结果上加上*
typedef decltype(lengthCompare) *func2;
//等价
typedef bool (*func2)(const string &, const string &));
- 重新声明f:把
void f(bool pf(const string &, const string &));
改为如下
void f(fun1);
//等价
void f(fun2);
返回指向函数的指针
- 声明返回函数指针的函数最简单的办法是使用类型别名
using F = int(int *, int);
using PF = int(*)(int *, int);
- 和形参不同,返回类型不会自动转换成指针,我们必须显示第将返回类型指定为指针
PF f1(int);
F *f1(int);
- 直接声明,不用类型别名
int (*f1(int)) (int *, int);
- 尾置返回类型
auto f1(int) -> int(*)(int *, int);
- decltype:牢记decltype返回函数类型而不是指针类型
bool lengthCompare(const string &, const string &);
deltype(lengthCompare) *getFcn(const string &);
vector<deltype(lengthCompare) *> vL;
一个元素是指向bool (const string &, const string &)
的指针的vector