在C++中函数也支持重载(函数名相同,但参数不同)和重写(继承时重写父类的函数)
一,函数基础
一个典型的函数的定义包括以下部分:返回类型,函数名,形参(0个或多个组成的列表),函数体。
形参以逗号隔开,位于一对圆括号之内。函数执行的操作在函数体内。
例如:求两数之和
int sum(int a, int b) {
int result = a + b;//局部变量,用于保存计算的结果
return result ;//返回结果
}
函数的调用:
int main(){
int a = 20;
int b = 40;
int c = sum(a,b);//形参调用
int d = sum(20,40);//实参数调用
cout << "a + b = "<<c<<endl;
return 0;
}
函数在调用时也可分为:形参调用和实参调用;在函数调用时要根据函数定义的参数类型,传入对应的类型参数才能正常的调用。
1,局部对象
在C++中不同作用域的对象都有对应的生命周期,名字的作用域是程序文件的一部分,名字在其中可见,对象的生命周期是程序执行过程中该对象存在的一段时间。
形参和函数体内部定义的变量统称为局部变量,他们对函数而言是局部的,仅在函数的作用域内可见,同时局部变量还会屏蔽外层同名的变量。
函数体之外的对象存在于程序的整个执行过程,在程序启动时创建,终止时销毁,局部变量的生存则依赖于定义的方式。
1)自动对象
函数体内部定义,调用函数时执行对象创建时,创建该对象,当函数执行到末尾时,销毁它,只存在于函数执行期间的对象。
例如形参就是一种自动对象,在函数开始时为形参申请存储空间,函数终止时就被销毁。
2)局部静态对象
在函数调用时创建,函数执行完成后依然存在的对象,使用static进行修饰,他的生命周期只到程序终止才被销毁。
例如:
int count(int num) {
static int res = 0 ;
return res + num;
}
int main() {
int a;
cin >> a;
cout << count(a) << count(a) <<endl;
return 0;
}
如上函数,多次调用count(a),其结果会基于上一次的结果进行累加,因为res 是static类型的,程序运行期间会一直在内存中。
2,函数的声明
函数只能被定义一次,但可以声明多次,函数的声明无需函数体,使用分号代替。函数的定义则是对函数的具体实现。
函数一般是在头文件中声明,在源文件中定义。
例如:
class study
{
public:
void setAge(int age); //函数的声明
private:
int age;
};
//函数的定义
void study::setAge(int age)
{
this->age = age;
}
二,参数传递
每次调用函数时,都会重新创建他的形参,并传入实参对形参进行初始化。
当形参数是引用传递或传引用调用时,引用形参是它对应的实参的别名。
1,传值参数
当实参的值拷贝给形参时,形参和实参是两个独立的对象。
当初始化化一个非引用类型的变量是,初始值被拷贝给变量,对变量的改动不会影响初始值。
例如:
int a = 10;//int 类型的初始变量
int b = a;//b 是a 的值的副本
b = 20;// b的值改变,不会影响a
函数对形参做的所有操作都不会影响实参。
2,指针形参
指针的行为和引用类型一样,当对指针执行拷贝是,拷贝是是指针的值,拷贝之后两个指针是不同的指针。
例:
void enlarge(int* pr) {
*pr = *pr * 10;
}
int main() {
int ab = 10;
int* pb = &ab;
enlarge(pb);
cout << "enlarge ab:" << ab << endl;
return 0;
}
把指针作为形参进行传递,然后修改指针所指向的对象,然后 ab的值也发生了变化,结果输出100。
3,传引用参数
对引用的操作实际作用于所引用的对象本身。
例如:
void reduce(int& pr) {
pr = 20;
}
int main() {
int ab = 10;
int ac = 20;
int &pr = ab;
reduce(pr);
cout << "enlarge ab:" << ab << ac<<endl;
return 0;
}
通过引用pr修改ab的值,最后打印结果ab的值也变成了20。
当操作毕竟大的对象或容器时,使用拷贝的方式效率低下或不支持,因此使用引用传递可以避免拷贝提升效率
注:函数无需改变引用形参的值时,使用常量引用。
4,const形参和实参
当const为顶层时,只能访问和拷贝,不能进行修改。
例如:
void read(const int pi) {
pi = 20;
}
当形参被const修饰后,就变成只能读取,不能修改,如上代码编译器会直接报错。
注:尽量使用常量引用,这样可以防止对象被修改。
5,数组形参
使用数组作为形参时,不允许拷贝数组,使用时将其转换为指针。
使用数组作为形参时有如下表现形式
void print(const int*);
void print(const int []);
void print(const int [10]);
注:使用数组作为形参时,也确保使用数组时不会越界。
使用数组作为形参时,需要结合begin和end来进行使用,这样可以有效的防止下标越界。
6,可变形参initializer_list
如果函数的实参数量位置但全部实参的类型相同,可以使用initializer_list
函数 | 描述 |
---|---|
initializer_list lst; | 默认初始化:T类型元素的空列表 |
initializer_list lst{a,b,c}; | lst 的元素数量和初始值一样多,lst的元素是对应初始值的副本,列表中元素是const |
lst2(lst); | 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后原始列表和副本共享元素 |
lst.size(); | 列表中的元素数量 |
lst.begin(); | 返回指向lst中首元素的指针 |
lst.end(); | 返回指向lst中尾元素下一位置的指针 |
和vector不同的是,initializer_list对象中的元素永远是常量值,无法改变initializer_list对象中元素的值。
例如:
void request(initializer_list<string> lst) {
for (auto beg = lst.begin();beg != lst.end();++beg)
{
cout << *beg << endl;
}
}
//使用时:
request({ "one","two","three" });
request({ "one","two","three","four" });
三,返回类型和return语句
return语句的作用:终止当前正在指向的函数并将控制权返回到调用该函数的地方
常见的表现形式有两种:
return;
return expression;
1,无返回值函数
无返回值的函数只能用在返回类型是void的函数中,在void的函数中不要求一定要有return语句,因为在代码执行到最后会隐式地执行return
如果无返回值函数,想要在代码中提前退出,则可以调用return回到函数调用的地方。
例如:
void change(int a,int b){
if( a > b)return;
int temp = a;
a = b;
b = temp;
}
2,有返回值函数
return语句可以返回函数的结果,只要函数的返回类型不是void类型。
return语句返回值的类型必须于函数的返回类型相同,或能隐式地转换成函数的返回类型。
int getMax(int a, int b) {
if (a == 0 || b == 0) return 0;
if (a > b)
{
return a;
}
else {
return b;
}
}
1)返回值的实现过程
返回一个值的方式和初始化要给变量或形参一样,返回的值用于初始化调用点的一个临时量,该临时量就是函数的调用结果。
2)不要返回局部对象的指针或引用
函数调用完成后,它所占用的存储空间也会随之被释放掉,局部指针或引用将会失效。
例如:
const string& change() {
string ret;
if (!ret.empty())
{
return ret; //返回局部对象的引用
}
else {
return "Empty";//返回了局部临时量
}
}
如上两种方式都是错误。
3,返回数组指针
因为数组不能被拷贝,所以函数不能返回数组,而是返回一个数组的指针或引用。
int arr [ 10 ] ;// arr是一个含有10个整数的数组
int * pi[10];//pi 是一个含有10个指针的数组
int (*pr)[10] = &arr;//pr 是一个指针,指向含有10个整数的数组
四,函数重载
同一作用域内的几个函数名字相同但形参列表不同。
例如:
void print(int a);
void print(int a,int b);
void print(int a,int b,int c);
编译器会根据传递的实参的类型和个数推断出调用的是哪个函数。
注:
1)重载和const修饰的形参是无法区分的,const只是一个修饰符。
2)当操作执行同一个操作时,所需参数不同时使用重载。
编译器对重载的处理
1)找到一个与实参最佳匹配的函数,并生成调用该函数的代码
2)找不到任何一个函数与调用的实参匹配,此时编译器就会报错
3)如果出现多个匹配的函数,就会出现二义性的错误
五,特殊用途语言特性
默认实参,内联函数,constexpr函数等
1,默认实参
函数在多次调用时使用了同样的形参,这是可以使用默认实参,这样调用时就可以省略掉
例如:
void print(string str = "Hello ,Word!") {
cout << str << endl;
}
int main() {
print();
print("Hello China");
return 0;
}
默认实参一般出现在头文件中,如果正常的形参和默认实参同时存在时,默认实参防止函数的最后面。
2,内联函数
在编译时,会被拷贝到调用的地方,减少函数调用的开销,使用时需要使用inline进行修饰
例如:
inline void print(string str = "Hello ,Word!") {
cout << str << endl;
}
int main() {
print("Hello China");
return 0;
}
注:内联机制适合函数规模较小,流程直接,频繁调用的函数,在内联函数中不要使用递归,因为有些编辑器不支持。
3,constexpr函数
修饰用于常量表达式的函数。
定义时,函数的返回类型及所有形参的类型都得是字面值类型,且函数体中必须有且只有一条return语句。
constexpr int new_sz() {
return 42;
}
constexpr int foo = new_sz();
执行该初始化任务时,编译器会把constexpr函数的调用替换成其结果值。
六,函数指针
函数指针指向的时函数而非对象。
1,函数指针的基本使用
例如:
void print(const string &str) {
cout << str << endl;
}
void (*pr)(const string&);
int main() {
pr = print;
pr("Hello Word");
return 0;
}
2,函数指针的赋值
函数指针的赋值也分为两种,一种直接赋值,一种使用解地址符的方式进行赋值
例如:
//定义一个函数,接收两个引用类型的参数
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
//定义一个函数指针
void (*pr)(int& a, int& b);
int main() {
int a = 10, b = 20;
pr = swap;//直接给函数指针赋值
pr(a, b);
cout << "直接赋值==。a:" << a << "--b:" << b << endl;
pr = &swap;//解地址符的方式赋值
pr(a, b);
cout << "去地字符赋值==。a:" << a << "--b:" << b << endl;
return 0;
}
3,函数指针作为参数
使用函数指针,可以把函数作为参数进行传递。
作为参数进行传递时,函数指针的声明也分为两种
1)隐式声明
void fun1(const int& a, bool pf(const string&, const string&)) {
}
2)显式声明
void fun2(const int& a, bool (*pf)(const string&, const string&)) {
}
在使用时,可以直接传入一个函数,和Lambda表达式类似。
3)简化函数指针的声明
函数指针的简化声明
void swaps(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
typedef bool(*FuncP)(const int&, const int&);
typedef decltype(swaps)* FunP2;
如上两个都是函数指针,他们是等价的类型。