大家好~我们接着【C++】C++入门知识详解(上)-CSDN博客来介绍另一些C++入门基础知识。
1.缺省值和缺省参数
缺省参数就是声明或定义函数时为函数的参数指定一个缺省参数。在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则,使用指定的参数。
有些地方把缺省参数也叫做默认参数。其实并不复杂,看下面的函数例子就能理解了。
void Func(int a = 0)
{
cout << a << endl;
}
我们定义这个函数时会给形参一个默认值,这个默认值其实就是缺省值,当我们调用这个函数时,传参和不传参情况如下。
Func();
Func(1);
我们可以看到,不传实参时,函数就用原本的a=0这个默认值作为形参;当给函数传参时,传的什么,形参就是什么。这里的0就是缺省值,a就是缺省参数。
缺省参数分为全缺省和半缺省。全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔、不能跳跃给缺省值。
//全缺省
void Func1(int a = 1, int b = 2, int c = 3)
{
cout << "a = " << a << " ";
cout << "b = " << b << " " << "c = " << c << endl;
}
//半缺省
void Func2(int a, int b = 2, int c = 3)
{
cout << "a = " << a << " ";
cout << "b = " << b << " " << "c = " << c << endl;
}
而且,带缺省参数的函数调用,C++规定必须从左往右依次给实参,不能跳跃、不能间隔给实参。
我们先看全缺省的函数Func1。有如下四种调用
Func1(); //什么都不传
Func1(4); //传一个实参
Func1(4, 5); //传两个实参
Func1(4, 5, 6);//传三个实参
半缺省的函数Func2,因为第一个参数没有给缺省值,所以必须传一个实参。有如下3种调用
Func2(4); //传一个实参
Func2(4, 5); //传两个实参
Func2(4, 5, 6);//传三个实参
缺省参数在实践中是非常有意义和价值的,在学习中我们可以体会到。
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
比如在头文件Func.h中,我们声明上面的函数Func1和Func2
void Func1(int a = 1, int b = 2, int c = 3);
void Func2(int a, int b = 2, int c = 3);
在源文件test.cpp中定义Func1和Func2
void Func1(int a, int b, int c)
{
cout << "a = " << a << " ";
cout << "b = " << b << " " << "c = " << c << endl;
}
void Func2(int a, int b, int c)
{
cout << "a = " << a << " ";
cout << "b = " << b << " " << "c = " << c << endl;
}
如果在test.cpp文件中函数定义的参数还是出现了缺省值,就会报类似下面这样的错误,显示重定义默认参数。
2.函数重载
C++支持在同一作用域出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。返回值不能作为函数重载的条件。
这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域出现同名函数的。
2.1 参数类型不同
同样是实现两个数的相加,一个是整数的相加,一个是小数的相加,在C语言中我们可能就会分成两个不同的函数来实现,比如add1实现整数的相加,add2实现小数的相加,有了函数重载,我们就可以用add一个函数名来实现这两个函数。
int add(int a, int b)
{
cout << "int add(int a, int b)" << endl;
return a + b;
}
double add(double a, double b)
{
cout << "double add(double a, double b)" << endl;
return a + b;
}
同名函数?那怎么调用呢?
其实,在函数调用时,会根据我们传给函数的实参的类型自动调用相应的函数。
add(1, 2); //传int型实参
add(1.1, 2.2); //传double型实参
单看函数名,我们就觉得自己在用同一个函数,其实并不是一个函数。这样函数用起来就很有可读性。
2.2 参数顺序不同
函数重载的参数也可以是类型相同顺序不同,看下面两个函数
void f(char c, int i) //先char,后int
{
cout << "void f(char c, int i)" << endl;
}
void f(int i, char c) //先int,后char
{
cout << "void f(int i, char c)" << endl;
}
调用时也是一样,根据传给函数的实参的类型自动调用相应的函数
2.3 参数个数不同
名字相同,参数个数不同也是函数重载。普通的例子在这里就不列举了,我们说一下特殊的。
看下面两个函数,一个函数无参,一个函数带参,这两个函数构成函数重载吗?
void f1()
{
cout << "void f1()" << endl;
}
void f1(int a = 10)
{
cout << "void f1(int a)" << endl;
}
构成函数重载。但是,调用时会存在歧义。
当我们调用时给函数传参,就会自动调用有参数的那个函数,这个没问题
那当我们不给函数传参时,应该调用哪个呢?含缺省参数的函数无参也可以调用啊。所以此时函数调用就会有歧义,程序会报错
所以大家在学习中要注意这些问题。
3.引用
3.1 引用的概念、定义及特征
引用不是新定义一个变量,而是给已存在的变量起个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。符号为&。(类型& 引用别名 = 引用对象)
引用的出现其实针对的是C语言中的指针。引用其实很好理解,比如在水浒传中,林冲外号"豹子头",李逵江湖人称"黑旋风"等,这些就是他们的别名,而不管是豹子头还是林冲,都代表的是同一个人,引用也是一个道理。我们来看具体程序。
int a = 0;
int& b = a; //给a取别名为b
int& c = a; //给a再取一个别名为c
我们可以不止取一个别名,一个变量可以有多个别名。我们还可以给别名取别名,如下。
int& d = b; //给别名b取别名为d
此时d相当于还是a的别名,我们看看a,b,c,d的地址,会发现它们的地址一摸一样。
这就验证了这句话,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间 。如果我对d加加呢?
会发现a,b,c,d全都加了,这也证明a,b,c,d是同一个,都是a。
引用的特征:
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,就不能再引用其他实体(引用不能改变指向)
前两个特征很好理解,我们重点说一下第三个特征。先看看下面这段代码。
int a = 10;
int& b = a;
int c = 20;
b = c;
思考一下,这里 b = c 是什么意思?是把b变成c的别名?还是c赋值给b?
这里其实是赋值,把c的值赋给b,b是a的别名,也就是把c的值赋给a。我们把a,b,c都打印出来看看 。
这也证明引用不能改变指向。我们还可以通过观察地址来看引用是否改变了指向。
可以看到a,b地址相同,b还是a的别名,和a共用同一块空间,而c只是赋值给b。
3.2 引用的应用
引用在实践中主要用于传参和做返回值,准确说就是用在函数。
1.函数传参其实是把实参拷贝给形参,形参是实参的临时拷贝,有了引用就可以减少拷贝。
2.引用从语法上来说,形参是实参的别名,形参也不会额外开辟空间,效率就得到了提高。
3.在函数中我们知道,形参的改变不影响实参,想要通过函数改变实参就要传地址对吧,学了引用后我们大部分情况就可以不用指针传地址实现了。
举一个最简单例子,函数实现交换两个数,在C语言中应该像下面这样传地址才能实现实参的改变。
void Swap(int* px, int* py)
{
int temp = *px;
*px = *py;
*py = temp;
}
当我们学了引用,就不需要指针了,我们直接将形参看作实参的别名
void Swap(int& rx, int& ry)
{
int temp = rx;
rx = ry;
ry = temp;
}
这里,rx就是x的别名,rx的改变就是x的改变;ry就是y的别名,ry的改变就是y的改变,rx与ry的交换就实现了x与y的交换 。
在能使用指针的地方比如说栈,队列等都可以尝试用引用,会方便很多。引用做返回值我们后续再讨论。
3.3 coust引用
const引用被const修饰的量
一个被const修饰的变量a,怎么给它取别名?
const int a = 10;
直接int& ra = a;绝对是不可以的。这是一个经典的权限放大。被const修饰的变量其实就是让这个变量变得只能读不能写,而int类型是可读可写的,别名ra也可读可写,a被const修饰只能读,而现在来一个可读可写的别名?这就让a权限放大了。
引用权限可以缩小,不能放大。那应该怎么给const修饰的变量取别名呢?像下面这样。
const int& ra = a;
const引用正常变量
没有被const修饰的变量b,可以直接用int& rb = b;来取别名
int b = 20;
那可以像下面这样吗?
const int& rb = b;
可以,这里就是引用权限缩小,一个可读可写的b,别名rb只能读。
但是这并不意味着b的权限缩小了,用b这个名字的时候还是可读可写,但是用rb这个名字的时候就只能读,但rb还是b,只是权限不同,就像你在家可以想脱鞋就脱鞋,在教室就不能随意脱鞋,但你还是你,只是权限不一样。
const引用常量
const引用还可以给常量取别名。比如说我要给10这个常数取别名。如果不加const就不行。
const int& rc = 10;
const引用临时对象
const引用还可以给临时对象取别名。比如给a+b这个表达式取别名,如果不加const就不行。
//这里a和b有没有const无所谓
const int a = 10;
int b = 20;
const int& rd = a + b;
因为表达式的结果会存在临时对象里面,临时对象 就是编译器需要一个空间暂存表达式的求值结果时,临时创建的一个++未命名++的对象,这个临时对象具有常属性。如果是 int rd = a + b;意思就是把a+b结果的临时对象拷贝给rd。
再看下面这个,我们怎么给double 类型的d取一个int类型的别名?
double d = 3.14;
int i = d;
上面这个d并不是直接赋值给i,d和i类型不同,这里叫隐式类型转换,隐式类型转换中间会产生临时对象存储。所以这里我们也不能直接用int& ri = d;取别名,还是要加const。
double d = 3.14;
const int& ri = d; //给d取int类型的别名
(临时对象的生命周期会和别名的生命周期一样)
3.4 引用和指针的区别
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
• 语法概念上引用是一个变量的取别名不开空间 ,指针是存储一个要量地址,要开空间。
• 引用在定义时必须初始化 ,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引用一个对象后,就不能再引用其他对象 ;而指针可以在不断地改变指向对象。
• 引用可以直接访问指向对象 ,指针需要解引用才是访问指向对象。
• sizeof中含义不同,引用结果为引用类型的大小 ,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
4.inline内联
(1)C语言实现宏函数会在预处理时替换展开,但是宏函数的实现很复杂很容易出错,且不方便调试,C++设计了inline目的就是代替C语言的宏函数。
(2)用inline修饰的函数叫内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。
(3)vs编译器debug版本下默认是不展开inline的,这样方便调试,debug版本想展开需要设置下面两个地方,
(4)inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
(5)inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
5.nullptr空指针
NULL实际是一个宏,在.c文件中定义一个指针,赋值为NULL,然后按住ctrl,点击这个NULL,就会跳转到如下代码处。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成O,调用了f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。
插入一个小知识
在C语言中void*的指针是可以转成任意类型的,比如
void* p1 = NULL;
int* p2 = p1;
而在C++中语法更加严格,上面的p1想赋值给p2,必须强制类型转换成int*。
void* p1 = NULL;
int* p2 = (int*)p1;
我们来看具体例子,下面这个函数重载,实参传入NULL时,会调用哪个函数?第二个?
void f(int x)
{
cout << "void f(int x)" << endl;
}
void f(int* p)
{
cout << "void f(int* p)" << endl;
}
结果调用了第一个函数。证明NULL在C++中其实是0。
C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
简而言之,就是在C++中的空指针变成了nullptr,不是NULL。
本篇到这里就结束了,拜拜~