Witheart... Follow your heart...
目录
Witheart... Follow your heart...
前言
本系列文章作为笔者学习C++的一个记录,希望可以坚持下去。目前使用C++Primer Plus这本书。
本系列文章适合有C语言基础的读者阅读。
学习内容
函数原型
需要函数原型的原因
为什么需要函数原型呢?难道编译器不能再文件中自行查找函数,了解函数是如何定义的吗?
这样做的问题一是效率不高;二是函数不在文件中,C++ 允许将一个程序放在多个文件中,单独对这些文件进行编译,然后再将它们组合起来。所以有的时候 main() 可能无权访问这些函数的代码,当这些函数位于非开源的库中也是如此。
C++ 中函数原型与 ANSI 原型区别
在 C++ 中,参数列表为空与在括号中使用 void 是等效的——这意味着函数没有参数。但是在 ANSI C 中,括号为空意味着不指出参数,而是在后面定义参数列表。在 C++ 中,不指定参数列表时应该使用省略号:
void say_bye(...);
通常,这种方法可运用于和接受可变参数的 C 函数(如 printf())交互。
参数与参量
参数(argument):实参
参量(parameter):形参
函数使用指针处理数组
参数传递
使用下面的定义作为参量原型
int* arr;
//或者
int arr[];
在函数头中使用指针指向数组后,指针和函数名指向同一个地址,但是不能使用 sizeof() 作用于该指针来得到整个数组的长度,因为这样只能得到指针变量的长度,而对数组名使用 sizeof() 得到整个数组的长度,所以传递数组给函数时,必须显式地传递数组长度。
const
为防止函数无意中修改数组的内容,可以在声明形参时使用关键字 const。
const int* arr;
这种表示说明 arr 是一个指针,指向 const int* 型。
使用数组区间处理数组的函数
通过传递两个指针给函数来操作数组,一个指针标识数组的开头,另一个指针标识数组的尾部。使用“超尾”概念来定义区间,标识数组结尾的参数将是指向最后一个班元素后面的指针。如:
int arr[20];
则 arr 和 arr + 20 标识了数组的区间。
具体的数组操作方式如下:
void func(const int* begin, const int* end)
{
const int* p;
for(p = begin; p!=end; ++p){
//...
}
}
指针与 const
const 指针可以指向常规变量也可以指向 const 变量;常规指针不能指向 const 变量,但可以使用强制类型转换完成这种操作。
注意,进入两级间接关系时,const 和非 const 混合的指针赋值方式将不再安全。如下:
const int** pp2;
int* p1;
const int n = 13;
pp2 = &p1;//pp2 指向 p1
*pp2 = &n;//使 p1 指向 n,绕开了编译器的限制
*p1 = 10;//使用了 p1 修改了 n 的值
上面的代码将非 const 地址(&p1)赋给了 const 指针(pp2),所以可以用 p1来修改 const 数据。
尽可能地使用 const
-
可以避免无意中修改了数据;
-
用 const 指针可以使函数可以同时处理 const 和非 const 数据,否则只能处理非 const 数据,所以当函数不需要修改数据时,使用 const 指针作为形参;
-
只要有一层间接关系,就可以使用 const避免无意中的数据修改,但是如果传递的元素不是基本类型,比如指针数组或者指向指针的指针,则不能使用 const。
函数与二维数组
形参声明
int data[3][4];
void func(int (*arr)[4], int size)
{
//...
}
或者使用:
int data[3][4];
void func(int arr[][4], int size)
{
//...
}
处理字符串中的字符的标准方式
while(*str){
//...
++str;
}
引用与指针
与其他声明指针的方式类似
array<double, 4>* p;
操作时,由于优先级关系,使用:
(*p)[i];
注意括号。
递归
函数自己调用自己,但是 C++ 不允许 main() 调用自己,而 C 语言允许。
递归在特定的编程(如人工智能中式一种重要的工具)。递归的一般编写方法如下:
void recurs(argumentlist)
{
statements1
if(test)
recurs(arguments)
statements2
}
只要 if 为 true,每个recurs() 调用都将执行 statement1,然后再调用 recurs(),而不会执行 statements2。当 if 语句为 false 时,当前的调用将执行 statements2,当前调用结束后,将返回上一级的 recurs(),然后执行它剩下的 statements2 部分,然后再返回再上一级的 recurs(),以此类推。
函数指针
与直接调用一个函数相比,使用函数指针作为参数可以允许该该函数在不同时间传递不同函数的地址,意味着它可以在不同的时间使用不同的函数。(回调函数,比如定时器中断回调,定时器中断后要执行什么函数将又用户决定,将此接口暴露给用户)。
函数的地址就是不带参数的函数名。
函数指针的声明
应该指出该指针所要指向的函数的返回值和特征标,如下:
double pam(int);//函数原型
double (*p)(int);//对应的函数指针
指针的声明只是用(*p)替换了pam,由于 pam 是函数,则 (*p)也是函数,则 p 就是函数指针。注意括号,没有括号以为着是一个返回指针的函数,而不是函数指针。
实际上,对于函数指针,可以使用两种方式使用该指针。如下:
P(5);
(*p)(5);
为什么 p 和 (*p)等价呢,因为人们在这个问题上有分歧,C++ 深谙中庸之道,两种都允许。
深入探讨函数指针
“函数指针的表示可能非常恐怖。”
函数原型:
const double* f1(const double ar[], int n);
const double* f2(const double ar[], int n);
const double* f3(const double ar[], int n);
上述函数的函数指针:
可以使用普通的声明方法:
const double* (*p)(const double ar[], int n);
也可以使用 auto 进行函数指针的声明:
auto p2 = f2;
包含指向上面三个函数的函数指针的数组:
const double* (*pa[3])(const double ar[], int n) = {f1, f2, f3};
为什么 [3] 放在这个地方呢?
要声明一个函数指针数组,首先,pa 是一个包含三个元素的数组的数组名,要声明这样的数组,首先需要使用 pa[3],而 [] 的优先级高于 *,所以 *pa[3] 表明 pa 是一个包含三个指针的数组的数组名,再加上其他的修饰,最终声明了一个函数指针数组。
这里可以使用 auto 吗?
不可以,因为 auto 不可以用于初始化列表,只能用于单值。
指向函数指针数组的指针:
由于函数数组数组名 pa 是指向函数中指针的指针,因此指向该数组的指针将是一个指向“指向指针的指针”的指针。非常恐怖。如下:
当该指向函数指针数组的指针为 pd 时,则 *pd 是数组,则用 (*pd)替换上面声明中的 pa,有:
const double* (*(*pd)[3])(const double ar[], int n) = &pa;
不过,可以使用 auto 来替代这种声明:
auto pd = &pa;
使用该指针来调用数组中的函数的方式如下:
//两种方式:
(*pd)[i](argu); //直接使用指针名代替函数名来调用函数
(*(*pd)[i](argu));//解引用函数指针来调用函数
声明时,可以使用 tyoedef 进行简化。如下:
typedef const double* (*p_fun)(const double* , int);//将这种函数指针定义为 p_fun
p_fun p1 = f1;
然后使用这个别名来创建函数指针数组:
p_fun pa[3] = {f1, f2, f3};
使用这个别名来创建指向函数指针数组的指针:
p_fun (*pa)[3] = &pa;
内联函数
通常,程序执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈,然后跳到标记函数起点的内存单元,执行函数代码,将返回值放入寄存器中,最后跳回到地址被保存的指令处。
而内联函数则不同,编译器将使用被调用函数的相应函数代码替换函数的调用,程序无需跳来跳去,而只需要顺序执行,所以运行速度比常规函数稍快,但是代价是内联函数会占用更多的内存,如果程序在 10 个不同的地方调用同一个内联函数,则会保存该函数代码的 10 个副本。
要使用内联函数,有两种方法:
- 在函数声明前加上关键字 inline;
- 在函数定义前加上关键字 inline。
通常的做法是省略原型,将整个定义放在提供原型的地方。
注意,内联函数不能递归。
引用变量
引用是已定义的变量的别名。定义如下:
int rats;
int & rodents = rats;
声明引用时,必须同时初始化,不能像指针一样在之后初始化。引用接近于 const 指针,一旦和某个变量关联起来,就将一直效忠于它。也就是说:
int & rodents = rats;
实际是下述代码的伪装表示:
int* const pr = &rats;
其中 rodents 和 *prx 相同。
按引用传递
按引用传递允许被调用的函数能够访问函数中的变量,而不是使用其副本。如果不想让函数改变传递的引用,则应该使用常量引用。
double func(const double & argu);
引用的传递很严格,如果形参是引用,则传递的应该是一变量,而不能是表达式。如下:
func(x + 3.0);
这种代码是不允许的,因为 x + 3.0 并不是变量。如果它是变量,就应该可以被赋值,如下:
x + 3.0 = 5.0;
但是实际上这种赋值的操作并不可行。
在早期的 C++ 中,传递表达式给引用的形参是可以的,此种情况下,编译器将创建一个临时的无名变量,并将表达式的值赋值给它,然后再按引用传递。在当前,仅仅当引用参数是 const 型的,编译器才会在下面两种情况下生成 const 引用:
- 实参类型正确,但不是左值;
- 实参类型不正确,但可以转换为正确的类型。
左值是可以被引用的数据对象,如变量、数据元素、结构成员、引用和解除引用的指针;非左值有字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。总之,常规变量和 const 变量都称为左值,因为都可以通过地址访问。
为什么当引用参数是 const 时时才允许生成匿名的临时变量呢?
因为如果不为 const 变量,且函数的目的是修改被引用的变量,如果此时生成匿名的变量,实际上修改的操作是对匿名的临时变量进行的,这样修改被引用的变量的目的无法达到。实际上,对于形参为 const 引用的 C++ 函数,如果实参不匹配,则其行为类似于按值传递。
右值引用(rvalue reference)(C++11)
使用 && 声明。右值引用可指向右值。可以用来实现移动语义(move semantics)。以前的引用称为左值引用。
返回引用
传统返回机制与按值传递函数参数类似,计算 return 后面的表达式,将这个值复制到一个临时位置,而调用程序使用这个值。 如:
传统返回
cout << sqrt(25.0);
计算的结果被复制到一个临时位置,然后传递给 cout。
返回引用
直接将计算的值传递给 cout,没有中间变量,效率更高。
返回引用时不要返回函数中的临时变量,因为函数运行后该临时变量将不复存在。
形参为 const string & 时,可以接受 C风格字符串。这是由于 C 风格字符串实际上是 const char*,而 string 类定义了一种 char* 到 string 的转换,而对于 const 引用,在恰当时机将会创建正确类型的临时变量,使用转换后的 C 风格字符串来初始化它,然后传递一个指向该临时变量的引用,这就是上文提到的 const 引用自动生成匿名临时变量的情况之一。
对象、继承和引用
ofstream 对象可以使用 ostream 类的方法,这使得文件输入输出的格式和控制台输入输出的格式相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承。在这里,ostream 是基类,而 ofstream 是派生类,派生类继承来基类的方法。
基类引用可以指向派生类对象,而无需进行强制类型转换。如 cout,被定义为接受 ostream & 的函数,同时可以接受 ofstream & 对象作为参数。
例子
使用同一个函数将数据写入文件并在屏幕上显示出来。如下:
void file_it(ostream & os)
{
os.setf(ios::showpoint);
os << "hello";
}
file_it(cout);//可以这样使用,使这个函数可以写入文件或者输出显示
file_it(fout);
此类函数可能设置了各种格式化状态,通过 .setf() 设置,如果不想在调用函数后格式化状态被改变,可以在进入该函数时先将格式化信息存储起来,离开函数时再设置回去。方法 setf() 返回调用它之前有效的所有格式化设置。ios_base::fmtflags 是存储这种信息所需的数据类型名称。代码如下:
ios_base::fmtflags initial;
initial = os.setf(ios_base::fixed);//存储刚开始的格式化信息
//...
os.setf(initial);
默认参数
默认参数是当函数调用中省略了实参时自动使用的一个值。
通过函数原型可以设置默认值:
char* left(const char* str, int n = 1);
没有提供 n 这个参数时,n 将被初始化为 1。
对于带参数列表的函数,必须从右向左添加默认值。也就是说,默认值应该全部放在参数列表的右边。使用默认参数时,实参被按从左到右的顺序赋值给相应的形参,不能跳过任何参数。如下面这样时非法的:
func(5, , 8);
函数重载
函数特征标(function signature)
如果两个函数的参数数目和类型相同,同时参数的排列属性也相同,则特征标相同。
函数重载
可以定义特征标不同的重名函数。注意,特征标不同是唯一条件,与函数返回值等其他函数特征无关。
传递的参数和函数的形参不同时,如果该函数由多个同名函数(进行了函数重载),则编译器会尝试强制类型转换,但是当转换有多种选择时,会导致二义性,此时这种函数调用将被视为错误。
注意,类型引用和类型本身被视为同一个特征标。因为如果同意此类函数重载,那么编译器在调用时将不知道使用哪一个函数,导致二义性错误。如:
void func(int & n);
void func(int n);
func(x);//此时编译器不知道使用哪一个函数
重载引用参数
当参数为引用时,可能有三种引用:常规引用、const 引用、右值引用。比如 const 引用的形参可以和常规引用配对,也可以和 const 引用配对;右值引用和 const 引用可以配对,也可以和右值引用配对。当重载使用这三种参数的函数时,编译器将使用最匹配的版本。