一个函数包括:
通过调用符号来执行函数。调用符号是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是用逗号隔开的实参列表,用这些实参初始化函数的形参。调用表达式类型就是函数的返回类型。
函数调用主要完成两个工作:
return语句在函数中有两项工作:
函数定义了一个语句块,语句块构成的作用域中定义的变量和形参都是局部变量。他们对于函数而言是局部的,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。(即局部变量和外层作用域同名的变量的修改互相不干扰,前提是局部有定义!而不是直接修改)
这个东西还是很有意思的,需要注意的是,静态的是函数的作用域,虽然函数执行完了,但在函数作用域内,该对象只会初始化一次。
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; }
一般在头文件中声明函数,在源文件中定义函数。
分离式编译,即当项目复杂时,由于很多时候实现和声明是分离的,有的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 #链接两个源文件形成可执行文件
参数传递分为:引用传递和值传递
对于值传递,形参和实参是独立的,即函数中形参的修改并不会影响实参。但并不是一定不能修改函数外的值。
对于指针而言,形参对于实参指针进行了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; }
用法和定义引用差不多,简单理解就是定义一个引用形参和实参绑定。
什么情况下用传引用的参数呢?
ans:如果函数无须改变形参的值,最好将其声明为常量引用
回顾:顶层const表示指针本身是一个常量(作用于对象本身);底层const表示指针所指的对象是一个常量
void fun(const int i); void fun(int i); //错误,因为两个函数虽然形式上有差异,但实际上两者的形参没有什么不同
const形参的使用和const本身的应用是差不多的。而常量形参可以接受非常量的实参,但反之不可以。这也是为什么尽量使用常量引用的原因,可以增加参数所能接受实参的类型。
数组有两个性质对于使用数组参数有影响:
以下3个函数定义的方式都正确
void print(const int*); void print(const int[]); void print(const int[10]); //10这个长度其实并没有影响,最后都是指针类型
单纯的指针并没有包含数组的长度,所以调用者还需要提供指针的长度信息。
void print(const char *cp) { if (cp) while(*cp != '\0') { std::cout<<*cp++; } }
这种技术适用于有明显标记的数据。
传递指向数组首元素和尾后元素的指针
void print(const int *beg, const int *end) { while(beg != end) { std::cout << *beg++; } }
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.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]
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}; }
一般函数的规定是:如果函数的返回类型不是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];
int even[] = {2,4,6,8}; int odd[] = {1,3,5,7}; decltype(odd) *arrPtr(int i) { return (i%2)?&odd:&even; }
确定调用的函数应该根据函数名和参数(形参数量,形参类别)来确定。
如果形参是某种类型的指针或引用,则通过区分其指向(底层)的是常量对象还是非常量对象可以实现函数的重载。但若实参是非常量的,优先考虑非常量的版本的函数。
重载函数的标注是要看重载后是否会让函数更容易理解
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); }
函数匹配:指把函数调用与一组重载函数中的某一个关联起来的过程,也叫做重载确定。通常,根据参数的个数和类型很容易确定,但是当类型是可以相互转换的类型时就比较困难。
当重载函数时有三种可能的结果:
void print(const string &); void print(double); void foobar() { void print(int); //函数内的局部作用域上声明的函数 print("Hello world"); //报错 }
在foobar函数内声明的print(int)隐藏了函数外(外层作用域)中的同名实体。
需要注意的一点是,对于函数的声明,多次声明一个函数是合法的,但在给定的作用域中,一个形参只能被赋予一次默认实参。
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函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr函数通常定义在头文件中,保证定义完全一致。
assert(expr);
对expr求值,如果表达式为假,assert输出信息并终止程序的执行。如果为真,则什么也不做。
assert的行为依赖与一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
可以使用一个#define语句在代码中定义NDEBUG,从而关闭调试状态。同时,很多编译器提供命令行选项定义预处理变量。
g++ -D NDEBUG main.cpp -o mian
除了C++编译器定义了静态的函数名字信息,预处理器还定义了另外4个对于程序调试有用的信息。
__func__;//函数名 __FILE__;//存放文件名的字符串字面值 __LINE__;//存放当前行号的整型字面值 __TIME__;//存放文件编译时间的字符串字面值 __DATE__;//存放文件编译日期的字符串字面值
重载函数的匹配对于函数的使用非常重要。
确定候选函数和可行函数:第一步就是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数一是与被调用的函数同名,二是其声明在调用点可见。
考察函数调用提供的实参:从候选函数中选出能被这组实参调用的函数,这些新选出来的函数称为可行函数。可行函数一是其形参数量与实参数量相等,二是每个实参的类型与实参类型相同,或者是能转化成形参的类型。
寻找最佳匹配:从可行函数中选出与本次调用最匹配的函数。逐一检查函数调用提供的实参,寻找类型最匹配的那个可行函数。精准类型匹配的要比需要类型准还的匹配更好。
编译器会依次检查每个实参以确定哪个函数是最佳匹配。如果有且仅有一个函数满足下列条件,则匹配成功,否则报二义性错误。
即将实参转换为形参的类型。重点是要确保类型转换后不会出错。
如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。即常量类型和非常量类型会进行区分,进行精准的匹配。当用非常量对象作为形参的时候,转换为常量需要进行类型转换。
函数指针指向的是函数而非对象。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
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)类型函数的指针
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作用于函数时,它返回函数类型而非指针类型!