在C++中,相对于C语言,我们引入了许多新的概念,新的机制,它们的到来让我们的编程方便了不少,它并没有多么难理解,但是比较零碎,在我看来它就是一些加分项,故此,我们通常也将其称为C++中的糖果。下面就让我为大家来介绍几个比较有意思的概念。
缺省参数
提到缺省参数,那么参数必定是和函数相关的内容,那么什么到底是缺省参数呢?
我就用一个当下比较火的网络红词来大体解释一下什么是缺省参数,那就是“备胎”。我相信大家都懂这个意思。现在我用专业语句解释一下这个概念。
- 概念:缺省参数就是在函数声明或者定义的时候为函数参数设置的一个默认值。当调用该函数的时候,如果没有指定该函数的实参值时,则默认使用默认值,否则就使用我们的实参值。
例:
void fun(int a = 10){
cout<< a << endl;
}
int main(void){
fun(); // 采用缺省值 a == 10
fun(20); //采用实参值 a == 20
}
全缺省参数
void fun(int a = 10,int b = 20,int c = 30){
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
半缺省参数
void fun(int a,int b = 10,int c = 20){
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
注意:
- 1、半缺省参数必须是从右往左缺省,不能间隔给出
- 2、缺省参数不能在函数的声明和定义中同时出现,只可出现一次。因为如果在声明或者定义中同时出现缺省参数,但是恰好两次给的值不一样,那么编译器就不知道到底该用哪个缺省值。
内联函数
提及内联函数,就不得不说一下C语言中的宏。我们经常把宏叫做预处理宏,因为在预处理阶段,要进行比较重要的一件事,就是进行宏替换,即在预处理阶段宏所处的位置进行代码展开。
例:
#define ADD(a,b) (a+b)
这就是一个简单的宏函数,它的功能就比较简单了就是简单的两个数相加。
宏
-
1、为什么要使用宏定义
对于一些简单的数学计算,比如相加相减等等,这些计算就非常简单,我们就没必要专门写一个函数来解决这个问题,因为一旦我们写了一个函数,那么调用该函数时,必然会存在函数的压栈问题,那么调用该函数就有一定的时间开销,影响我们的效率。所以我们就可以利用宏来解决这个问题,因为宏在预处理阶段就会进行宏替换,将代码展开,那么相对于普通函数而言,宏函数没有函数压栈的问题,节省时间,提高我们的程序运行效率。 -
2、宏定义的缺陷
因为宏是在预处理阶段进行处理的,所以不能进调试,这就带来了许多问题,我们自己写的宏函数很有可能就有错误,例如
#define ADD(a,b) a+b
//这就是一个简单的加法,但是如果我们是这样用的
ADD(3,4)*2;
//我们预期的结果是14,但是结果却是11,这是因为什么呢?因为进行宏替换的时候,是直接将代码展开即 3+4*2 所以这就造成了和我们的预期结果不一样的情况。
那么我们该如何避免宏错误呢?最直接的方法就是给我们的变量加上括号
#define ADD(a,b) (a+b)
这种方法就可以在一定程度上避免宏错误,但是其实作用也不大,因为我们还是很容易就漏写了括号。正是因为我们不方便调试宏,它的可读性比较差,没有类型安全检查等缺陷,所以在C++中我们引入了内联函数。
内联函数
内联函数与宏具有相同的作用,以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
例:
inline int add(int a,int b){
return a + b;
}
内联函数特性
1、内联函数是一种以空间换时间的方式,省去函数压栈的时间开销,所以代码很长或者函数中有循环递归等不适用内联函数。
2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
宏和内联函数的区别
内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
例:
int a = 10;
auto b = a; //b会根据a的类型,自动推导它的类型为int
我相信有人会问那么这个auto 有啥作用呢? 其实它很简单,就是为了方便我们的书写, 当然这里看不出来它的优势,那是因为我们还没有遇到很长的变量类型,假如我们以后要写一个迭代器的类型,它的类型名会特比长,那么这时候auto的优势就很明显了,我们只需要在变量前加一个auto即可,编译器会自动推导它的类型,特别方便我们的书写。
注:auto在使用时必须对其初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
auto的使用细则
1、auto、auto*及auto&
int a = 10;
auto b = &a;
auto* c = &a;
auto& d = a;
//其中auto与auto*没有区别,但auto&声明引用变量时必须加&符号
2、当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1,b = 2;
auto c = 3,d = 4.5;//此句出错
3、auto不能使用的场景
auto不能推导形参的类型
auto不能用来声明数组
auto不能定义类的非静态成员变量
实例化模板时不能使用auto作为模板参数
4、基于范围for的auto关键字
平常我们要打印一个数组的值,如下:
int array[] = {9,5,2,7,3,8,6};
for(int i = 0 ; i < sizeof(array) / sizeof(int); i++){
cout<<array[i]<<endl;
}
//基于auto的打印
for(auto e:array){
cout<<e<<endl;
}
//它的作用与正常的for()循环一样,取出数组中的每个值,然后将每个值赋值给auto定义的变量e,然后将e的值打印出啦。
//假如我们要改变利用auto关键字改变数组的值,我们应该如何去做
for(auto& e : array){
cout<< e *= 2 <<endl;
}
指针空值nullptr
c++98中的指针空值
编写C/C++程序时,我们要有良好的编程习惯,声明一个变量时,我们最好给变量赋一个合适的初值,否则将会出现不可预估的错误。比如一个未初始化的指针,我们经常都是按照如下的方式给指针变量赋值
int* p = NULL;
int*p1 = 0;
其实NULL与0是一样的,NULL其实是一个宏,它被定义在的C头文件(stddef.h)中,如下:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到NULL的值其实就是0或者(void*)0。但是我们不论采用何种定义方式,都会存在不可避免的麻烦,如下:
void fun(int)
{
cout<<"fun(int)"<<endl;
}
void fun(int*)
{
cout<<"fun(int*)"<<endl;
}
int main()
{
fun(0);
fun(NULL);
fun((int*)NULL);
return 0;
}
其中fun(NULL)本意是想调fun(int*)这个函数,但是它却调了fun(int)这个函数,因为NULL在预处理阶段就被替换成了0,所以结果与我们的预期不符,造成了这种结果
nullptr
为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什么不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可能不太相同,而且直接扩展NULL,可能会影响以前旧的程序。因此:为了避免混淆,C++11提供了nullptr,即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr_t被定义在头文件中:
typedef decltype(nullptr) nullptr_t;
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。