目录
- 一、缺省参数
- 二、函数重载
- 1. 函数类型不同
- 2. 参数个数不同
- 3、函数类型顺序不同
- 三、引用
- 1、引用的概念和定义
- 2、引用的功能
- 2.1 功能1: 做函数形参,修改形参影响实参
- 2.2 功能2: 做函数形参,减少拷贝,提高效率
- 2.3 功能3:引用做返回值类型,修改返回对象
- 2.4 功能4: 引用做返回值类型,减少拷贝,提高效率
- 3、引用的特性
- 4、`const`引用
- 5、指针和引用的关系
- 6、`inline`
- 7、`nullptr`
【C++】入门基础【上】<–请点击
个人主页<—请点击
C++专栏<—请点击
一、缺省参数
- 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)。
下面我们通过代码来认识一下缺省参数。
void func(int a = 10)
{cout << a << endl;
}
我们知道,在C语言中函数定义中变量是不能给值的,但在C++
中却可以,这时候,给定的这个值就是缺省值,那这个参数就是缺省参数。
那了解了缺省参数是什么,那么它在使用的时候又该怎么用能?请看代码:
func();
func(5);
此时我们要调用我们定义的函数,并且一个不给实参,一个给实参,让我们一起看一下代码的运行情况。
可以看到,没给实参的,函数默认使用了缺省值,而给定实参的,函数使用的是给定的实参,而在C语言中,如果我们不给实参但函数又需要实参时,此时程序就会报错,所以C++的缺省参数优化了这一问题。
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。
C++
规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
如果你这样定义代码:
void func1(int a = 100, int b)
{cout << a<< " "<< b << endl;
}
会报错:
因为半缺省参数必须从右往左依次连续缺省。
-
带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
像上图打算只给第二个传实参,跳过第一个时就会报错。 -
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
void func2(int a = 100);
void func2(int a = 100)
{cout << a << endl;
}
应改为:
void func2(int a = 100);
void func2(int a)
{cout << a << endl;
}
二、函数重载
我们知道C语言中不支持在同一作用域
中出现同名函数
的,它会报错,但在C++
中是被允许的。C++
支持在同⼀作用域中出现同名函数,但是要求这些同名函数的形参不同
,可以是参数个数不同
或者类型不同
。如果全部相同那在C++
中也是不被允许的。
1. 函数类型不同
void func(int x, int y)
{cout << x + y << endl;
}void func(double x, double y)
{cout << x + y << endl;
}
测试结果:
从上面的测试结果,可以看出函数的调用成功而且正确。
2. 参数个数不同
void f()
{cout << "hello world!" << endl;
}void f(int a)
{cout << a << endl;
}
测试结果:
注意
:这里的第二个函数的参数不能带缺省值,如果带缺省值的话,当我们调用f()
时,第一个函数和第二个函数都满足,此时程序会报出下面的错误:
这样编译器就会不知道调用谁,所以我们写重载函数的时候一定要注意区分它们,不能让它们存在歧义。
3、函数类型顺序不同
void fd(int a, double b)
{cout << "fd(int a, double b)" << endl;
}void fd(double b, int a)
{cout << "fd(double b, int a)" << endl;
}
测试结果:
三、引用
1、引用的概念和定义
引用不是新定义⼀个变量,而是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同⼀块内存空间。
引用的用法:类型& 引⽤别名 = 引⽤对象;
C++
中为了避免引入太多的运算符,会复用C语言
的⼀些符号,引用和取地址使用了同⼀个符号&
,大家注意使用方法角度区分就可以。
#include<iostream>
using namespace std;int main()
{int a = 10;//b和c是a的别名int& b = a;int& c = a;cout << a << endl;b++;cout << a << endl;c++;cout << a << endl;return 0;
}
结果:
#include<iostream>
using namespace std;int main()
{int a = 10;//b和c是a的别名int& b = a;int& c = a;//d也是a的别名int& d = b;cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
它们的地址都是一模一样的。
2、引用的功能
2.1 功能1: 做函数形参,修改形参影响实参
我们在C语言阶段实现交换函数的时候是用指针来实现的,现在引用也可以起到这样的作用。
#include<iostream>
using namespace std;void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}int main()
{int x = 10;int y = 20;Swap(x, y);cout << "x=" << x << endl;cout << "y=" << y << endl;return 0;
}
交换结果:
2.2 功能2: 做函数形参,减少拷贝,提高效率
不使用引用时,值传递会触发原对象的拷贝,如果对象比较大(如包含复杂成员,动态内存或嵌套结构),拷贝操作的时间和空间开销显著。
而当使用引用时,引用就是原对象的别名,传递时不会产生拷贝,直接操作原始对象,省去了拷贝构造的开销,尤其对大型对象效果显著。
2.3 功能3:引用做返回值类型,修改返回对象
#include <iostream>
using namespace std;int& func(int* a, int n)
{return a[n];
}void print(int* a,int n)
{for (int i = 0;i < n;i++){cout << a[i]<<" ";}cout << endl;
}int main()
{int a[11];for (int i = 0;i < 11;i++){a[i] = i + 1;}print(a, 11);func(a, 2)+=10;print(a, 11);
}
从上面代码中可以看出,func
函数的返回值类型是引用充当的,这样的好处是可以更改返回对象。
运行结果:
从运行结果可以看出数组中下标为2
的位置被更改了。
2.4 功能4: 引用做返回值类型,减少拷贝,提高效率
我们知道当函数调用结束时函数栈帧会被销毁,那么其中定义的局部变量的生命周期也就结束了,自然也会被销毁,当函数的返回值是函数中定义的局部变量时,编译器会将返回值拷贝下来,然后储存在临时变量中作为返回值。
假设有如下函数:
int func()
{int set = 10;return set;
}
那既然传引用返回可以更改返回对象,那传值返回可以吗?下面我们来试一下。我们还是使用传引用返回的代码,但是将传引用返回改为传值返回。
发现程序会报出以下错误:
这是因为函数返回的是值,而在函数返回值之前同样会将返回的值拷贝下来,储存在临时变量中进行返回,而临时变量它具有常性,是不可修改的,所以才会报出以上错误。
下图是它们三者之间的区别:
所以说引用做返回值类型,减少了拷贝,提高了效率。
产生临时变量的情况:
当出现类型转换的时候也会产生临时变量,像
double
类型d=1.5
,转化为int
类型的x
这种情况,会产生一个临时变量存储转换结果,然后再将临时变量赋值给x
。
产生临时变量的情况有以下几种:
类型转换、值传递、表达式求值
等等。
其中值传递就是我们上图展示的情况,表达式求值例如:
int a = 1;
int b = 9;
int c = a + b * 10;
计算
b*10
时生成临时int
,再与a
相加生成临时int
,最后赋值给c
。
不安全的引用写法:
int& func()
{int set = 10;return set;
}
原因:set
是局部变量,func
结束后,set
就销毁了,返回它的别名本质也是一种类似野指针的行为。
3、引用的特性
- 引用在定义时必须初始化;
- ⼀个变量可以有多个引用;
- 引用⼀旦引用⼀个实体,再不能引用其他实体。
引用无法改变指向所以在链式结构中无法替代指针,这样的场景下必须使用指针。
4、const
引用
可以引用⼀个const
对象,但是必须用const
引用。const
引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,而不能放大。
权限放大的错误样例:
const int x = 10;
int& y = x;
最初定义的x
的本意就是x
不可更改,但却使用int&
引用x
这就导致了冲突,它的权限被放大了。
正确的使用:
const int x = 10;
const int& y = x;
注意
:以下这种情况中没有权限的放大。
const int x = 10;
const int& y = x;
int z = y;
有人在学习完成引用后他们会认为上面这段代码涉及到了权限的放大,但上面代码的意图是定义一个变量z
,并将y
值赋值给z
,只是一个简单的赋值操作,注意不要混淆了。
权限的缩小是被允许的:
就像老师对你说,下课不准出校门,但你连教室门都不出这种情况一样。
int x = 10;
const int& y = x;
5、指针和引用的关系
- 语法概念上引用是⼀个变量的取别名,不开空间,指针是存储⼀个变量地址,要开空间。
- 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
- 引用可以直接访问指向对象,指针需要解引用才能访问指向对象。
sizeof
中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4
个字节,64
位下是8
个字节)- 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
指令汇编角度:引用是使用指针实现的。
int x = 10;
int& y = x;
int* py = &x;
转到指令汇编:
引用的下面两行是引用语句的汇编代码,而指针下面两行是指针语句的汇编代码。我们从上图可以看出两者一模一样,所以进一步印证了引用是使用指针实现的。
6、inline
用inline
修饰的函数叫做内联函数,编译时C++
编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立函数栈帧了,就可以提高效率。
C语言实现的宏函数也会在预处理时替换展开**,但是宏函数实现很复杂且很容易出错,还不能调试,C++
设计inline
目的就是替代C语言的宏函数。**
正常的函数:
int add(int x, int y)
{int sum=x + y;return sum;
}
执行语句:int ret = add(2, 3);
时,它的反汇编代码是这样的:
图片中有一个call
指令,这个指令是调用add
函数,说明函数没有在预处理时展开。
inline
修饰的函数:
inline int add(int x, int y)
{int sum=x + y;return sum;
}
执行语句:int ret = add(2, 3);
时,它的反汇编代码是这样的:
从上图可以看出它没有调用函数,也就是没有创建函数栈帧,而是在预处理阶段
就展开了,像C语言的宏函数一样。这样就可以提高效率。
inline
对于编译器而言只是⼀个建议,也就是说,你加了inline
,编译器也可以选择在调用的地方不展开,不同编译器关于inline
什么情况展开各不相同,因为C++
标准没有规定这个。inline
适用于频繁调用的短小函数,对于递归函数,代码相对多⼀些的函数,加上inline
也会被编译器忽略。
inline
不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline
被展开,就没有函数地址,链接时会出现报错。我们知道编译器生成可执行程序会经过预处理、编译、汇编、链接
等过程,而在链接过程代码中的普通函数的地址需要到XXX.o
符号表中去寻找,因为他们不会展开所以它们的函数地址会进入符号表中,当你正确使用inline
修饰函数,即声明和定义不分离时,inline
修饰的函数调用的地方已经正常展开了,而当你声明和定义分离时,由于inline
修饰的函数的地址它本身不会进入XXX.o
符号表,又因为你没有inline
修饰的函数定义,此时函数调用的地方就没有正常展开,编译器寻找地址的时候又找不到,此时就会报链接错误。
声明和定义分离的错误情况:
F.h
#include <iostream>
using namespace std;inline int add(int x, int y);
F.cpp
#include "F.h"inline int add(int x, int y)
{int sum = x + y;return sum;
}
test.cpp
#include"F.h"int main()
{int ret = add(2, 3);return 0;
}
链接错误:
而当你声明和定义不分离,再将F.cpp
中的定义删去,(因为此时F.h
中已经有了,如果你这里不删除的话它依旧会报错,因为出现了两个一摸一样的函数主体),即:
F.h
#include <iostream>
using namespace std;inline int add(int x, int y)
{int sum = x + y;return sum;
}
这样就可以正常展开了。
拓展
:inline
与static
所修饰的函数都具有内部链接属性,不会进入XXX.o
符号表中,所以不会造成C2084
类型错误:
7、nullptr
NULL
实际是⼀个宏,在传统的C头文件stddef.h
中,可以看到如下代码:
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif
#endif
上面这段代码是条件编译指令,感兴趣的小伙伴点击–>【C语言】编译和链接、预处理详解
C++
中NULL
可能被定义为字面常量0
,或者C
中被定义为无类型指针(void*)
的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,例如:
#include <iostream>
using namespace std;void f(int n)
{cout << "f(int n)" << endl;
}
void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);f(NULL);return 0;
}
这段代码的执行结果是:
本想通过
f(NULL)
调⽤指针版本的f(int*)
函数,但是由于NULL
被定义成0
,调用了f(int x)
,因此与程序的初衷相悖。f((void*)NULL);
调用会报错。
为了解决这个问题,C++11
中引入了nullptr
,nullptr
是⼀个特殊的关键字,nullptr
是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr
定义空指针可以避免类型转换的问题,因为nullptr
只能被隐式地转换为指针类型,而不能被转换为整数类型。
#include <iostream>
using namespace std;void f(int n)
{cout << "f(int n)" << endl;
}
void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}int main()
{f(0);f(NULL);f(nullptr);return 0;
}
这样就解决了这个问题。所以在C++
中初始化指针为空,会用nullptr
这个关键字初始化。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~