C++ Primer(第五版)第五章-函数读书笔记。
比较有意思的:
-
数组传参时退化为指针
-
数组类型;返回数组指针的多种方式;
-
函数指针;返回函数指针的多种方式;
-
函数调用过程
函数使用
调用运算符
,即$()$
来调用。调用是,第一步便是使用实参(arguments)去隐式初始化函数的形参(params)。形参中是每个参数,必须有类型,但可以没有变量名!(模板时可能只需要类型名)但不管有没有变量名,参数都要传递过去。
void f(int , int * , int &) ; // 声明, 只需类型,不用加变量名
return
要执行两步,第一是用返回的值初始化接收值(如果有),其次跳回主调函数。当然,从汇编角度就更好理解了吧?是先弹栈,再压返回值?
-
局部自动变量与局部静态变量
局部变量,包括形参和函数内部定义的变量。 所以除了全局变量,就是局部变量了吧。
局部静态变量,就是使用
static
关键字修饰的 。与之相对的,称为自动变量。静态变量虽然是局部的,但是却只会初始化一次,且与整个程序生命周期相同。库函数中的
strtok
函数,就使用了静态变量来记住上次分割的位置。书上的例子,记录调用次数:
size_t count_calls() { static size_t ctf = 0 ; return ++ctf ; }
自动变量就是常见的普通变量了,“随控制路径”创建变量,超出作用域后就销毁。
二者初始化也有区别——局部自动变量,如果不显式初始化,就会执行默认初始化。对内置类型的局部变量,就是随机初始化(或者说不被初始化,仅申请空间)。静态变量则会执行值初始化,即初始化为0 ;
全局(自动)变量,也会执行值初始化,即初始化为0 .
-
函数声明
声明定义到头文件,定义(实现)在源文件。定义时需要引入头文件。头文件有必要加编译保护头。
可声明多次,但只能定义一次。
-
分离式编译
cc -c including.cc # 生成including.o / including.obj cc -c main.cc # 生成 main.o / main.obj cc main.o including.o -o main # 生成main / main.exe
-
参数传递
记住,
实参到形参传递的过程
等价于用实参初始化形参
void f(int param_a , const string & param_b) ; int argument_a = 2 ; f(argument_a , "ni hao a") ; <==> int param_a = argument_a ; const string & param_b = "ni hao a" ;
最佳实践: 使用引用避免拷贝。
void f(const string & str , const vector<int> & vec) ;
注意: 形参中的顶层const将被忽略。
顶层const是指: 1. cont int a ; 2. int * const p = &a ;
故 void f(const int a) ; 与 void f(int a) 在编译器看来是一样的!所以,这样“定义”将报错!
在形参中定义顶层const,保证了在函数中不能修改该形参!这与实参能否被修改是没有关系的!
-
传引用、常量引用
传递引用参数的好处:
-
避免拷贝
-
有时只能传递引用——IO类不支持拷贝(值传递)
非常量引用的限制:不能将const对象、字面值、需要类型转换的对象作为引用传递
经过测试,临时变量在GCC下也不能作为引用参数传递。 报的错误为:no known conversion for argument 1 from ‘[class_name]’ to ‘‘[class_name]&’ . 这个错误信息似乎在暗示引用参数传递存在隐式的转换,即类型到类型的引用的转换。或者一种猜测,临时变量被认为是const的,因为当我们形参设为const引用时,该语句就合乎语法了。
常量引用:
如果函数不会改变参数的值,推荐传递常量引用。
bool is_in_sentence(const string &str , char target_char) ;
原因:
-
避免给调用者错觉。
-
定义为常量引用能够接受的参数更广。
如果定义为普通引用,则 const对象 , 字面值 , 需要类型转换的对象 都不能够作为参数传递。
-
-
数组形参
数组两个性质:
-
不允许拷贝(赋值)
即 int a[2] = {1,2} ; int b[] = a ; // ERROR !
-
常常退化为指针
上述两个性质决定了:如果以传值方式定义数组形参,那么 数组就会退化为 指向数组头元素的指针。
所以,在以值传递形式定义数组形参时,等价于定义一个指针类型。
void f(int a[]) ; void f(int *a) ; void f(int a[10]) ;
上述都是等价的。注意,尽管第三个尽管定义了数组维度,但是一点用都没有… 因为会被转换为指针!
书上接着讲了在函数中如何获取数组的长度(知道数组结尾呢)?方法有3:
-
使用标识量
即字符数组,使用’\0’作为字符数组的结尾。
-
传递头指针和尾指针
可以使用begin和end函数。
void print_array(int * array_beg , int * array_end) ;
-
传递数组长度
这个在以前的C++中、以及C中最直观、常用
言外之意,估计现在用begin、end方法更多吧。因为这样就保证了和迭代器的一致性接口了。的确更美观了。
引用方式传递的数组
例子:
void f(int (&a)[10])
一定要注意!
-
对数组的引用的写法:
int (&a)[10]
, 如果写作int &a[10]
是非法的!因为此表示a是10个引用元素构成的数组——而我们知道,引用时不能构成数组的。我们定义的是对数组的引用! -
数组的维度不可缺少。
如果缺少,将报错:
“错误:形参‘a’包含了指向具有未知边界数组‘int []’的引用”
这是因为传递引用时,数组实参初始化形参时保证了数组未退化!
所以要遵循数组引用的规则:
int a[4] = {1,2,3,4} ; int (&b)[4] = a ;
即要保证维度确定且一致! 这个需要注意!!
书上说,以后学习了模板,可以解决这个问题!就是可以传递不定长的数组。我想本质上,就是为相应的调用生成一个确切长度的函数吧。这也是模板干的事情嘛…所以本质是没有变的。
关于数组的扩展
感觉对数组一直没有搞懂。
做了以下测试:
int a[4] = {1,2,3,4} int b[] = a ; int b[4] = a ;
第一个赋值语句报错:
错误:数组必须为一个由花括号包围的初始值设定所初始化
第二个赋值语句报两个错误:
错误:初始值设定无法决定‘b’的大小
错误:数组必须为一个由花括号包围的初始值设定所初始化
由此得到结论,如编译器报错所言,数组是不能直接用数组赋值的!
我们平时常用的是这样的:
int *b = a ;
所以常常造成了数组可以赋值的错觉。
其实,上面真正的意义是——将数组a退化为指向数组第一个元素的指针,然后赋值给指针b。所以其本质是指针的赋值。
数组的退化
上面也提到了,如果我们使用值传递的方式传递数组参数,那么数组将会退化为指针。
经过测试,我认为数组只能退化一次——即多维数组,只能是第一维发生退化。所以,在向函数传递多维数组时,只有第一维可以忽略维度大小,后面的都不能忽略——第一维退化为指针了,维度信息自然就损失了,所以编译器不能也不会检测;不过后面的维度都保持了数组的类型不退化,而数组是需要明确指定维度大小的,所以编译器要求必须指定维度。
数组与指针
上面说了数组的退化,这里就是要明确: 数组是数组,指针是指针。数组可以退化为指针,但是数组不是指针。
二维数组作为参数传递
考虑如下:
void f(int **a) ;
输入变量是指针的指针,它与普通二维数组没有关系!——可以认为,只有在C风格字符串数组时它才是有意义的。
void f(char **a) ; char *a[] = {"str1" , "str2"} ; f(a) ;
因为一维数组退化为指针,变为指向数组首元素的指针,而首元素又是字符串数组,也是一个指针,所以满足char **的定义。而且,由于是硬编码字符串,且连续,所以内存中可以认为是连续的,可以在函数中使用 a[0] , a[1]来访问。如果我们我们将其他整型指针放到一个数组中,传递给函数时退化为一个int**的变量, 但是不能使用a[0][0]来访问——这可能照成未知的结果!因为基于a的指针偏移,可能指向的不是其真正的内存空间!
上面对应的等价定义就是:
void f(int *a[]) ; void f(int **a) ;
正确的二维数组传递方式,有两种:
void f(int (*a)[10]) ; // (1) void f(int a[][10]) ; // (2)
比较上面的定义与前面
int **
的差别,(1)中多了括号,表示其与变量是一个整体——a是指向一个int[10]类型的指针,即维度为10的整型数组的指针。(2)中指定了第二维的维度,因为第一维退化,其实其就(1)就是等价的。想想,还是(2)更加直观和好理解啊。(1)需要做数组向指针退化的逆向过程,即a值指向int[10]的指针,其实对应实参,其可能是int[10]的数组——数组的数组,就是二维数组咯。还有一种,就是对于二维数组,直接传递一个头元素指针即可。
void f(int *a) ; int a[2][2] = { {1,2} , {3,4} } ; f(a) ;
上述是可行的。我们要知道,多维数组在内存中,是一行一行按序依次存储的。所以,我们可以根据行列去访问它, 具体就是 a + row * col_size + col 。
总结以上,有关数组的知识:
-
数组是复合类型,元素的类型、元素的个数(维度)共同决定数组的类型。
int[4] , int[5] 是两种类型!虽然他们都是数组
-
数组值传递会退化为指向头元素的指针。多维数组只有第一维会退化。
-
数组与指针不是一回事!
虽然数组可以退化为指针,指针可以用下标访问符
[]
去访问元素。但,这也些都是编译器提供的隐式操作。数组退化可以认为是语言的局限,或者是兼容C语言的负面影响;指针用下标访问,可以认为是在数组退化的条件下,提供给程序员的便利。
它们都是在具体上下文下才有联系。脱离上下文去谈数组与指针的联系,就是耍流氓。
-
多维数组值传递,有两种定义方式: T [][d1][d2] / T (*)[d1][d2]
-
-
不定参数 / 可变参数
C系的可变参数,如printf系列的实现,使用了
...
在C++中不推荐使用该方法,因为该方法: 1. 仅支持C与C++交集的数据类型(即C++仅是继承了C的语法,不愿意扩展到C++上);2. 无类型检查
C++系,如果可变参数的类型相同,则使用
initializer_list
类型;如果类型不同,则使用后面介绍的可变参数模板
关于initializer_list :
-
标准库中的类,无需额外的include。
-
与vector使用类似
initializer_list<int> il{1,2,3,4} ; initializer_list<int> il_empty ; initializer_list<int> il_copy = il ; initializer_list<int> il_cc(il) ; for(auto ite = il.begin() ; ite != il.end() ; ++il ){} ; cout << il.size() << endl ; // no more
-
内容只读
il[2] = 4 ; // Error (il.begin()) = 4 ; // Error void processing_multi_var(const initializer_list<int> &il) { for(auto & i : il) { i = 10 ; // Error cout << i << endl ; } }
上述函数报错:错误:向只读形参‘i’赋值
用途: 书上举了例子,用于传递不定量的错误信息字符串。
-
-
函数返回值
-
如何返回
存在一定的疑问。
一种可能:首先有return处的值初始化一个函数定义域内的满足函数返回类型的临时变量。该临时变量再被压栈方式返回给调用点处。
另一种: return出的值直接返回到调用处,由调用处做类型转换等。
第一种似乎更有可能。
主要是书上有这么一个例子:
const string & ret() { return "Empty" ; }
书上说这个是错误的!因为返回了局部变量的引用。可知,函数必须先在函数定义域内构造一个string类型的临时变量,并用字面值”Empty”去初始化它,然后再返回该临时string的引用。所以第一种似乎更有可能。
-
注意事项
不能返回临时变量的指针;不能返回临时变量的引用。
因为函数运行完毕后对应的内存空间销毁,指针无效,引用无效。
-
C++0x支持返回列表
vector<int> return_list() { return {1,3,4} ; }
如上,返回一个列表(按照1的理解,其实用列表初始化了vector临时变量再返回该vector,故本质仍然是返回一个单变量对象)。
如果返回类型是内建基本类型,列表中只能有一个元素。
int return_int() { return {1} ; }
将其认为是列表初始化好了。
-
关于main函数
-
main函数是不支持递归调用的!
-
main函数即使声明为返回整型,也可以不写return语句。
编译器会隐式插入一条: return 0 ;
返回宏定义的状态码(在cstdlib中定义):
return EXIT_FAILURE ; return EXIT_SUCCESS ;
-
-
-
返回数组的指针或者引用
因为数组不支持拷贝,所以不能返回数组本身;
可以返回一个退化的指针。
但是如果想要就是返回一个数组的指针或者引用,有4中方法可以做到。
首先说明,如下的函数定义时错误的:
int (*)[5] return_array_pnt() { int a[5] = {} ; return &a ; }
为了构造返回数组指针的情况,返回了局部数组的指针。编译器会给出警告: 警告:返回了局部变量的‘a’的地址 [-Wreturn-local-addr] 这里仅作为演示。
会报两个莫名的错误:
expected unqualified-id before ‘)’ token expected initializer before ‘return_array_pnt’
总的还是格式不对吧(规约出错了吧)。
接下来介绍这4中正确的方法:
-
使用别名 /*** * Method 1 typedef int intArr5[5] ; ****/ using intArr5 = int[5] ; intArr5 *return_array_pnt(){}
两种定义别名的方法,
typedef
是C的传统,using
应该是C++ 11新标准。我觉得using语义更加清晰,不容易记错。
-
使用尾置返回类型
标准的C++ 11 新标准,非常直观:
auto return_array_pnt() -> int (*)[5]
使用
auto
和->
关键字。注意后面的*的括号,前面关于数组的指针和退化的指针有提及。 -
使用如下奇妙的格式
int ( *return_array_pnt() )[5]
之前从未见过。不是C++ 11 的新标准,不知道c支不支持。
经测试,使用gcc 4.8.3 20140911 (Red Hat 4.8.3-9) (GCC) 成功编译C语言版本。
这个是 int (* name)[d] 的拓广,也很好理解。不过就是很少看到用在函数上。看到很兴奋,不知道还有多少没有见过的东西~
-
使用
decltype
关键字非常trick的方法。
int b[5] ; decltype(b) *return_array_pnt()
因为decltype返回了数组的类型,所以这个本质与方法1——别名没有什么区别。
应该在模板编程时很有用。
以上都是返回数组的指针。返回数组的引用类似,如
auto get_array_ref() -> int (&)[5] ; int ( &get_array_ref() )[5] ;
一定要注意括号!特别是尾置返回类型,括号是必须的,否则就是引用的数组,通不过编译。
此外,获取数组的引用,一般都是返回非局部变量的,如传入参数的引用(传入参数也是引用),应用场景应该还是比较小的。
-
-
函数重载
重载,只发生在相同作用域,有相同的函数名、不同的参数的情况下。
-
如果作用域不同,那么最近的作用域内的名字将会覆盖上级作用域内的所有同名函数
void print(string info) ; void print(initializer_list<string> &info) ; int main() { void print(int infoID) ; print(2) ; }
如上,mian函数中(局部作用域)的print函数就覆盖了main函数上层(全局作用域)的两个print函数;
注意,C++中也是支持在局部作用域中定义函数的!
-
参数不同,包括参数类型不同或者参数个数不同
这里要注意的就是const的事情。
前面说过,顶层const对应编译器来说与没有const是相同的。
所以
void copy_str(const string ori_str) ; void copy_str(string ori_str) ;
是等价的,不构成重载;
但是,不影响底层const:
void copy_str(const string & ori_str) ; void copy_str(string & ori_str) ;
注意到引用的区别——换成指针也是类似的。cost的指针是顶层的,指针指向的是const,就是底层的const,能够作为重载的区分。
-
函数的返回值不会影响编译器对重载函数的判断
-
-
内联函数和constexpr函数
内联函数
格式:使用
inline
关键字,仅当声明与定义同时存在时才有效;一般放入.h
头文件中。编译过程:编译器在编译时在调用内联函数的地方展开函数,而不是生成调用函数的代码。
适用范围:规模较小、流程直接、频繁调用的函数。
优点:消除了定义函数时函数调用开销;消除了不定义函数时代码的繁琐。
注意:inline只是向编译器发出内联请求,编译器可以忽略该请求!如,编译器一般不支持递归函数的内联;会放弃规模较大的函数的内联请求。
内联函数与宏定义的异同:
-
同
都是展开代码!二进制文件中无源代码中的调用逻辑。
-
异
-
宏在预处理阶段展开,内联函数在编译过程中展开。
-
宏本质就是字符串替换——是基于字符串的,处于源代码层次,不涉及编译过程、语言特性 ; 内联函数本质仍然是函数,需要满足语法规则,是语言特性。
-
比较弱的一点,宏肯定会展开替换,但内联不一定。
-
constexpr函数
constexpr int new_sz(){ return 42 ;}
如上,一般constexpr函数就是这样的(唯一)类似格式。
明确constexpr函数的本质:在编译过程展开函数,且(必须)得到一个常量表达式。
首先,constexpr可以认为是内联函数的应用——编译器隐含的将constexpr定义为内联的,这样编译器才能够在编译过程中展开它!因此它也需要定义和声明写在一起,最好放到头文件中。
其次,内联函数中,实际执行的代码只能是
return
语句!不能有变量定义、操作等语句,最多含有别名声明等!因为我们要明白,函数展开仅能返回一个常量表达式,不能包含多余的语句。以下语句:
constexpr size_t scale(size_t cnt){ return 2*cnt ;} int arrValid[ scale(2) ] ; // right int i = 2 ; int arrInvalid[ scale(i) ] ; // wrong
第一个调用正确,因为展开后就是
2*2
的常量,第二个展开是2*i
,i不是常量,表达式不是常量,所以报错。(当然,一般说来g++不会报错!但这只是编译器扩展,不是被标准所支持的!!)注意到上面定义函数时不会检查返回值是否是常量!所以,其实对于单纯constexpr函数的编译检查,仅仅包括:
-
函数返回类型、参数类型都是字面值类型(即int、double、const char * , char 等 非类类型)
-
只有return语句(如前所述,也支持别名、using等不含实际操作的语句)
实测constexpr
1. constexpr size_t get_sz() { int i = 55 ; return 32 ; } 报错: body of constexpr function ‘constexpr size_t get_sz()’ not a return-statement 2. constexpr size_t get_sz(size_t i) { return 32 * i ; } int main() { int i = 4 ; int a[get_sz(i)] = {} ; } g++下也报错了: 可变大小的对象‘a’不能被初始化 3. 对2做调用修改,去掉初始化: int main() { int i = 4 ; int a[get_sz(i)] ; } 成功编译! 后来测试了下 int i = 4 ; int a[i] = {} ; 在g++下也会报错! 看来之前一直存在误解啊!—— g++也是不支持非常量数组的初始化的,但是支持其声明。所以说,这样程序在后续操作时就很有可能内存错误了! 我认为这是g++的BUG或者不稳定的地方,还不如向MSVC一样,直接禁止非常量数组的声明。
-
-
调试帮助(Assert , 调试预定义变量)
Assert
#include <cassert> assert(expr) ;
这是一个宏!!
定义在
assert.h
中,关键如下:linux gcc 版本: # define assert(expr) \ ((expr) \ ? __ASSERT_VOID_CAST (0) \ : __assert_fail (__STRING(expr), __FILE__, __LINE__, __ASSERT_FUNCTION)) (__assert_fail 为 extern 的一个函数,可能定义在了链接库中,没有细找) msvc版本: #ifdef NDEBUG #define assert(exp) ((void)0) #else #ifdef __cplusplus extern "C" { #endif _CRTIMP void __cdecl _assert(void *, void *, unsigned); #ifdef __cplusplus } #endif #define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) ) #endif /* NDEBUG */ void _assert(int test, char const *test_image, char const *file, int line) { if (!test) { printf("Assertion failed: %s, file %s, line %d\n", test_image, file, line); abort(); } }
上述两个版本,写法不同,但都是宏定义~ (写得挺有意思的,gcc看起来更加C-sytle , msvc简直奇技淫巧,不过很酷炫,js中貌似常这么写)
注意
NDEBUG
这个控制变量,如果我们编译时,使用cc -D NDEBUG test.cpp
那么生成的代码就不含检查的语句!这就是宏定义的优势!
所以,assert被认为是调试时的手段。
cc -D NDEBUG test.cpp
,使用-D
选项,等价于在代码中写#define NDEBUG
调试时常用的常量
常量名 意义 __func__
函数名称 __FILE__
文件名(编译时的路径) __LINE__
行号 __TIME__
编译时时间 __DATE__
编译时日期 上面的常量,都是编译器为每个函数定义的局部变量,类型是
const char *
assert报错不能使用try…catch捕获
try{ assert(cin) ; } catch(exception e) { cout << e.what() ; }
上述代码是无意义的!!
从上面assert的实现可知,其不是throw出来的异常,而是直接调用
abort
函数暴力退出的。try并没有什么用。我认为这是C++ 完全兼容 C语言造成分裂的一个例子。(Python中,assert测试错误抛出AssertionError,可以用try捕获)
但是,我觉得只要消除这种理解歧义,就不会有什么不妥。
assert
初衷本就不是用在release version的!其更多是用于调试,在发布时使用-D NDEBUG
取消该断言行为。而try则是为了在运行过程中处理异常,以便增强函数的鲁棒性!二者的目的、初衷不是相同的,混用其实是乱用…如果需要在运行时判断变量应该满足的属性,应该使用 if + try .
-
函数匹配
包含两个过程:
-
获得候选函数集
根据函数名、作用域
-
获取可行集
根据参数匹配程度
重载函数情况下,包含一个选择顺序:
-
精确匹配
参数类型一致、参数个数相同或者在有省略参数的情况下保持个数合法。
数组退化被认为是精确匹配的。
-
有自动类型转换的匹配
计数转换次数,转换次数越少的优先匹配;
存在相同的,则报
二义性
错误退出。
-
-
函数指针
函数类型,是由它的返回类型和参数列表确定的。
比如: int (int , int) ; int sum(int , int) ; // sum 就是类型为 int (int , int)的函数类型。
函数指针指向的是函数类型。
比如: int (*)(int , int) ; int (*sum_p)(int , int) ; // sum_p就是类型为 int(*)(int , int)函数指针类型。
函数指针的定义非常模糊,既可以用取地址符也可以不用;既可以用解引用符,也可以不用。
int sum(int a , int b){ return a + b ; } int main(int argc , char *argv[] ) { int (*plus_p)(int , int) = &sum ; int (*plus_e)(int , int) = sum ; // 右端可以有取地址或无取地址,但左边必须是指针形式! cout << (*plus_p)(1,1) << endl ; // 解引用优先级低于函数调用,括号是必须的! cout << plus_p(1,1) << endl ; cout << (*plus_e)(1,1) << endl ; cout << plus_e(1,1) << endl ; return 0 ; }
返回函数指针
与返回数组指针完全一致。
首先,不能返回函数实体,就像不能返回数组一样。
using func = int (int , int) ; func get_func() { return sum ; }
上述代码报错:
错误:‘get_func’声明为返回一个函数的函数
接着说返回函数指针的几种方式(与返回数组指针类似):
// 使用 using using funcP = int (*) (int , int) ; funcP get_sump() { return &sum ; } //使用 typdef typedef int (*funcP_typedef) (int , int) ; funcP_typedef get_sump_typedef() { return &sum ; } //使用 尾置返回类型 auto get_sump_rear() -> int (*) (int , int) { return &sum ; } //直接定义 // 不加括号就错了,编译器认为返回的是 // int* (int , int)的函数实体,报错:错误:‘get_sum_directly’声明为返回一个函数的函数 int (*get_sum_directly())(int , int) { return &sum ; }
关于函数指针的博客嗯,让我们彻底搞懂C/C++函数指针吧