《C++ Primer》读书笔记-第六章

矫情吗;* 2022-05-23 02:19 345阅读 0赞

函数是命了名的代码块,我们通过调用运算符执行相应的代码

声明:返回类型、函数名、形参列表、分号
定义:返回类型、函数名、形参列表、函数体
调用:函数或函数指针,实参列表

通常在头文件中声明,在代码文件中定义

  1. //fact.h
  2. int fact(int val);
  3. ///
  4. //fact.cpp
  5. #include "fact.h"
  6. int fact(int val)
  7. {
  8. int ret = 1;
  9. while (val > 1)
  10. {
  11. ret *= val--;
  12. }
  13. //当遇到return语句时,函数执行结束,同样完成两项工作:
  14. // 返回return语句中的值
  15. // 交回控制权
  16. return ret;
  17. }
  18. ///
  19. //main.cpp
  20. #include <iostream>
  21. #include "fact.h"
  22. using namespace std;
  23. int main()
  24. {
  25. //调用运算符是一对圆括号,它作用于函数或指向函数的指针
  26. // 函数的调用完成两项工作:
  27. // 实参初始化形参
  28. // 控制权转移,主调函数的执行被暂时中断,被调函数开始执行
  29. int j = fact(5);
  30. //实参是形参的初始值
  31. // 在函数体内部定义的变量都是局部变量,它们的作用域都是块作用域
  32. // 在所有函数体外定义的对象存在于程序的整个过程,在程序启动时被创建,在程序结束时被销毁
  33. // 如果想延长局部变量的生命周期,可以将其定义为静态变量(出生日期不变,死亡日期延长到程序终止)
  34. cout << j << endl;
  35. system("pause");
  36. return 0;
  37. }
  38. #include <iostream>
  39. using namespace std;
  40. void func();
  41. int n = 1; //全局变量
  42. void main()
  43. {
  44. static int a; // 静态局部变量
  45. int b = -10; // 局部变量
  46. cout << "a:" << a
  47. << " b:" << b
  48. << " n:" << n << endl;
  49. b += 4;
  50. func();
  51. cout << "a:" << a
  52. << " b:" << b
  53. << " n:" << n << endl;
  54. n += 10;
  55. func();
  56. }
  57. void func()
  58. {
  59. static int a = 2; // 静态局部变量
  60. int b = 5; // 局部变量
  61. a += 2;
  62. n += 12;
  63. b += 5;
  64. cout << "a:" << a
  65. << " b:" << b
  66. << " n:" << n << endl;
  67. }

程序中主函数main()两次调用了func()函数,从运行结果可以看出,程序控制每次进入func()函数时,局部变量b都被初始化。而静态局部 变量a仅在第一次调用时被初始化,第二次进入该函数时,不再进行初始化,这时它的值是第一次调用后的结果值4。 main()函数中的变量a和b与func()函数中的变量a和b空间位置是不一样的,所以相应的值也不一样。关于变量作用域和可见性的进一步讨论见第6 章。
静态局部变量的用途有许多:可以使用它确定某函数是否被调用过。使用它保留多次调用的值。

  1. 对静态局部变量的说明:
  2. (1) 静态局部变量在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,存储在动态存储区空间(而不是静态存储区空间),函数调用结束后即释放。
  3. (2) 为静态局部变量赋初值是在编译时进行值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的 值。而为自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
  4. (3) 如果在定义局部变量时不赋初值的话,对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符型变量)。而对自动变量来说,如果不赋初 值,则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
  5. (4) 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的,也就是说,在其他函数中它是“不可见”的。

值传递和引用传递

  • 如果形参是引用类型,它将绑定到对应的实参上
  • 否则,将实参的值拷贝后赋给形参

指针也遵从上面的规则,调用函数时将指针形参的值拷贝后赋给指针形参,它们是两个指针,但是指向同一个对象

传递引用可以避免对象的拷贝,因此建议尽量使用引用传递

如果不需要改变引用形参的值,最好使用常量引用,它能接受的实参类型比普通引用多

  • 普通引用只接受同类型的对象作为初始值
  • 常量引用可以用同类型对象、表达式、字面值初始化

如果函数需要多个返回值,可以使用引用形参来返回额外信息

const形参和实参

  1. void fcn( const int i);
  2. void fcn( int i );

并不能构成函数重载,因为和其它初始化过程一样,会忽略掉形参的顶层const。也就是说实参是常量或非常量都可以

数组形参

数组有两个特殊的性质

  • 不支持拷贝
  • 数组名通常会自动转化为指针

也使得数组形参变得特殊

  1. void print( const int* );
  2. void print( const int[] );
  3. void print( const int[10] );

主观上我们会以为这三个是不同的定义,然而后面两个也会自动转化为const int *来处理

在调用时,实参可以是数组名,也可以是整形指针

因为形参只是整型指针,所以数组的维度是没有传递到函数中的。有三种方法来解决这一问题

  • 传递首元素和尾后元素的指针
  • 增加一个参数,传递数组的大小
  • 使用数组引用形参

    标准库的begin()和end()函数是C++11新标准引入的函数,可以对数组类型进行操作,返回其首尾指针,对标准库容器操作,返回相应迭代器。

    标准库容器的begin()和end()成员函数属于对应类的成员,返回的是对象容器的首尾迭代器。

    新标准库的begin()和end()函数可以让我们更容易的获取数组的首尾指针(注意尾指针是最后一个元素的下一个地址)

数组引用形参

  1. void print(int (&arr)[10] );

不过这种定义也限制了我们只能传递维度为10的数组

多维数组

前面提到多维数组其实就是数组的数组,首元素是指向数组的指针

  1. void print(int matrix[10][10], int rowSize);

为什么要传递rowSize进去呢?

我们知道数组会自动转化为指针,这里也不例外,上面的定义的本质是

  1. void print(int (*matrix)[10], int rowSize);

因此需要rowSize来指明二维数组的第一个维度

main函数的形参

main函数是熟悉数组形参最好的例子

  1. int main(int argc, char *argv[]) {}

第二个形参被声明为char *的数组,意味个我们可以传递多个字符串,而具体个数由argc指定

也可以定义为

  1. int main( int argc, char **argv ) {}

返回一个值的方式和初始化一个变量或形参的方式完全一样

返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果

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

  1. const string &manip()
  2. {
  3. string ret;
  4. if( !ret.empty() )
  5. {
  6. return ret;
  7. }
  8. else
  9. {
  10. return "Empty";
  11. }
  12. }

上面的两处返回都是错误的

  • ret是局部变量,函数执行结束后就被销毁了,因此不可以引用
  • “Empty”将用于初始化调用点的一个临时量,该临时量是函数调用的结果,同样的,我们也不能引用一个临时量

直接进行成员访问

如果函数返回指针、引用或者类的对象,可以使用函数调用的结果访问结果对象的成员

  1. auto sz = shortString( s1, s2 ).size();
  • 一方面和调用运算符的优先级和结合律有关系
  • 另一方面应该和临时量也有点关系,我的理解是在对临时量进行成员访问,正确吗?

引用返回左值

如果函数返回的是引用类型的对象,可以直接对函数调用的结果进行赋值

  1. getVal( s, 0 ) = 'A';

实际是对引用所绑定的对象赋值

列表初始化

  1. vector<string> process()
  2. {
  3. return {"Hi", "bye"};
  4. }

这个我们在刷Leetcode 01的时候用到了

main的返回值

如果main函数不写return语句,编译器会自动为我们加上返回0的return语句
但是建议都加上

递归

函数调用了自身

  1. int fac( int val )
  2. {
  3. if( val > 1 )
  4. {
  5. return fac( val - 1 ) * val;
  6. }
  7. reutrn 1;
  8. }

一定会有个if,保证有一条路径是不包含递归调用的,不然就死循环了

返回数组指针

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

回想前面指向数组的指针的定义

  1. int (*p2)[10] = &arr;

声明一个返回数组指针的函数同样也很复杂

  1. Type (*function(parameter_list)[dimension]

但是形式上和定义指向数组的指针类似

  1. int (*func(int i))[10];

这种写法难免有些复杂,而复杂容易产生错误,我们有三种方式简化

  • 类型别名
  • 尾置返回类型
  • decltype

类型别名

  1. typedef int arrT[10];
  2. arrT *func( int i );

尾置返回类型

C++ 11新标准

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

decltype

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

不难看出这种方法使用范围有限

其中形参列表不同是指

  • 形参数量不同
  • 或数量相同但形参类型不同

注意:

  • 类型别名不构成重载
  • 顶层const不构成重载

MZF: 我们不应该追求语法而强用重载,函数名还是要尽量保证见名知义的好

const_cast和重载

如果函数的形参和返回值等是底层const,我们可以利用const_cast和重载创建出函数的非const版本

在写重载函数时注意修改形参的const,否则无法构成重载

  1. const string &Test( const string &s1 )
  2. {
  3. return s1;
  4. }
  5. string &Test( string &s1 )
  6. {
  7. auto &sResult = Test( const_cast<const string&>( s1 ) );
  8. return const_cast<string &>( sResult );
  9. }

这里可能不太明显,如果Test的实现很长的时候,这种技巧的优势就明显了。

重载与作用域

仅在同一作用域内才会构成重载

内层作用域中声明的函数将隐藏外层作用域中声明的同名实体(和变量没什么区别)

  • 默认实参
  • 内联函数
  • constexpr函数
  • 调试帮助

默认实参作为形参的初始值出现在形参列表中
如果某个形参被赋予了默认值,它后面的所有形参都必须有默认值
如果在调用时省略了实参,则使用默认实参初始化形参

默认实参声明

允许多次声明同一个函数,给不同的形参添加默认实参
在给定的作用域中,一个形参只能被赋予一次默认实参

  1. string screen( int width, int height, char title = ' ' );
  2. string screen( int width, int height, char title = '*' );
  3. string screen( int width = 24, int height = 80, char title );

第二条声明语句是错误的,一个形参只能被赋予一次默认实参
第三条声明语句是正确的,可以看到它并没有再次给title设置默认实参

MZF:

  1. 这里的示例代码对原书进行了修改,说实话原书的示例我没看懂,谁给解释解释?
  2. 没能理解c++支持这种语法有什么用,一次把默认值都声明好不就行了么

默认实参初始值

  1. int wd = 80;
  2. char def = ' ';
  3. int ht();
  4. string screen( int width = ht(), int height = wd, char title = def );
  5. void f2()
  6. {
  7. def = '*';
  8. //def改变了默认实参的值
  9. int wd = 100;
  10. //wd隐藏了外层定义的wd,但没有改变默认值
  11. string window = screen();
  12. }

下面的两句话很好的解释了这个现象

  1. 用作实参的名字在函数声明所在的作用域内解析
  2. 求值过程发生在函数调用时

也就是说

  1. screen只会找同一作用域内的变量作为实参,所以后来定义的局部变量wd根本是不可见的
  2. 默认实参只是代替了实参,但是初始化形参的时机没有变,还是发生在函数调用时。因此改变全局变量def的值后,默认实参也发生了改变

MZF: 代码同样有少的修改,原书声明函数时不指明形参名,默认实参有什么用?

内联函数

一次函数调用实际包含着一系列工作:

  • 调用前要先保存寄存器,并在返回时恢复
  • 可能要拷贝实参
  • 程序转向一个新的位置继续执行

把函数声明为内联函数可以避免这一系列的开销

函数声明前加上inline关键字就可以了

声明为内联函数后,在编译时将函数在每个调用点上“内联地”展开

因为要展开,所以内容长的函数不适合内联

constexpr函数

函数的返回类型必须是字面值类型
所有形参的类型必须是字面值类型

constexpr函数被隐式地指定为内联函数

调试帮助

assert预处理宏

需要引入cassert头文件

  1. assert( expr );

如果表达式的值为假(即0),assert输出信息并终止程序的执行

NDEBUG预处理变量

当然,我们并不会希望程序发布的时候assert也发挥作用,时不时的终止程序的执行

如果定义了NDEBUG,则assert什么也不做

我们也可以编写自己的调试代码

  1. #ifndef NDEBUG
  2. cout << "debug" << endl;
  3. #endif

预处理器还定义了4个对调试很有用的名字

  1. __FILE__
  2. __LINE__
  3. __TIME__
  4. __DATE__

其中形参列表不同是指

  • 形参数量不同
  • 或数量相同但形参类型不同

注意:

  • 类型别名不构成重载
  • 顶层const不构成重载

MZF: 我们不应该追求语法而强用重载,函数名还是要尽量保证见名知义的好

const_cast和重载

如果函数的形参和返回值等是底层const,我们可以利用const_cast和重载创建出函数的非const版本

在写重载函数时注意修改形参的const,否则无法构成重载

  1. const string &Test( const string &s1 )
  2. {
  3. return s1;
  4. }
  5. string &Test( string &s1 )
  6. {
  7. auto &sResult = Test( const_cast<const string&>( s1 ) );
  8. return const_cast<string &>( sResult );
  9. }

这里可能不太明显,如果Test的实现很长的时候,这种技巧的优势就明显了。

重载与作用域

仅在同一作用域内才会构成重载

内层作用域中声明的函数将隐藏外层作用域中声明的同名实体(和变量没什么区别)

内容

  • 声明函数指针
  • 函数指针作为形参
  • 函数指针作为返回值

声明函数指针

指针指向某种特定的类型,那么函数的类型是什么呢?

函数的类型由它的返回类型和形参类型共同决定,与函数名无关

  1. bool lengthCompare( const string&, const string& );

该函数的类型是

  1. bool ( const string&, const string& )

那么可以用下面的形式声明函数指针

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

即使用指针替换函数名即可

使用函数指针

函数指针有以下特殊:

  1. 函数名会自动地转换成指针,取地址符不是必须的
  2. 可以直接使用指向函数的指针调用函数,解引用不是必须的

    pf = lengthCompare;
    pf = &lengthCompare;

调用

  1. bool b1 = pf( "Hello", "goodbye" );
  2. bool b2 = (*pf)( "Hello", "goodbye" );
  3. bool b3 = lengthCompare( "Hello", "goodbye" );

函数指针形参

  1. void useBegger( const string &s1, const string &s2,
  2. bool pf( const string &, const string & ));
  3. void useBegger( const string &s1, const string &s2,
  4. bool (*pf)( const string&, const string& ));

上面两个函数都是合法的
我们在使用函数指针作形参时
可以显示的将形参定义成指向函数的指针
也可以直接使用函数类型,会自动转换为函数指针

使用类型别名或decltype简化

  1. //函数类型的别名
  2. typedef bool Func(const string&, const string&);
  3. typedef decltype(lengthCompare) Func2;
  4. //函数指针的别名
  5. typedef bool(*FuncP)(const string&, const string &);
  6. typedef decltype(lenghtCompare) *FuncP2;

返回函数指针

  1. int (*f1(int))(int *, int);

这样看上去复杂且难以理解,同样我们有多种方式来简化

  1. 类型别名
  2. decltype
  3. 尾置返回类型

到这里前六章的内容我们就完全看完啦!

后面会找一些练习来做,目前想到的有两个方面

  1. leetcode
  2. 使用opencv库做一些简单的图像处理

发表评论

表情:
评论列表 (有 0 条评论,345人围观)

还没有评论,来说两句吧...

相关阅读

    相关 c primer读书笔记 第一

    1.就编程语言而言,可移植性代表什么? 答:可移植性意味着这个语言在一个系统上所编辑的可运行的程序在另外一个系统上不用改或是只需改一点点就能运行