关于C++初识的基础知识点在初识(一)的时候我们主要总结了命名空间,输入输出的概念,缺省参数,内联函数,关键字auto 的用法;
在本文中我们主要总结C++中的两个重要知识点函数重载,引用的概念以及nullptr的用法;
一.函数重载:
1.为什么会存在函数重载???
我们知道在C语言中根据语法的概念,在同一个作用域或文件下不能声明相同名字的函数,如果一旦同名编辑器就会报错,如果在工程中就会有很多的麻烦,为了不造成这种问题通常函数名可能会很长,而且很复杂;所以在C++中就引入了函数重载来解决需要根据不同的数据类型调用不同名的函数,而导致编程人员在写程序时不方便的问题;
2.什么是函数重载???
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题;
3.关于函数重载的例子;
int Add(int a,int b){
}
int Add(char a,int b){
}
//这两个函数构成重载,因为函数参数的类型不同;
int Function(int a, char b){
}
int Function(char a,int b){
}
//这两个函数构成重载,因为函数参数的顺序不同;
void Test(){
}
void Test(int a){
}
//这两个函数构成重载,因为函数参数的个数不同;
void Test()
{}
int Test()
{}
//这两个函数并不能形成重载,因为函数重载与返回值类型无关;
问题:判断以下两个函数是否重载???
double Add(double a,double b){
return a+b;
}
double Add(const double a,const double b){
return a+b;
}
该函数不能够成重载;因为const所修饰的双精度浮点数a和b也只是一个数;它与第一个函数的参数在参数类型,参数顺序和参数数量上都是相同的;所以不能构成重载;
void Test(int *p){
}
void Test(const int *p){
}
该函数能够成重载;因为两个参数指向的对象是不同的即参数类型不同;int*p指内存某个数的地址;而const int *p表示地址所对应的内容,参数不同;
4.前面我们提到在C语言中不支持重载,所以在C++引入重载的概念;为什么C语言中不支持重载,而C++中支持重载呢???
--------------这是由C++的底层特性实现的:
翻译过程一般分为: 预处理—-编译——汇编——链接
预处理:展开头文件,宏替换,去掉注释,条件编译等工作;---------生成.i文件
编译:语法检查,生成汇编代码;---------- 生成.s文件
汇编:将汇编代码转成机器码 ;---------- 生成.o文件
链接:将之前生成的文件链接到一起,生成可执行文件;----------生成.out文件
如果有多个.c文件在链接之前各文件都是独立向下进行的,各文件之间没有交集 ,所以支持重载问题就出在链接这个阶段上;
C言在链接的时候根据函数名找要调用的函数,而C++则是根据函数名和参数类型来寻找要调用的函数即函数名修饰规则;
在C语言和C++中对函数名字的处理规则不一样。编译器在对函数编译完成后,实际上已经对函数的名字进行修改了;
举例:
int Add(int a,int b){
return a+b;
}
double Add(double a,double b){
return a+b;
}
//这两个函数是构成重载的;
在C语言中,编译器把这两个函数的名字都修改成了_Add,这两个函数无法区分,所以C语言中无法支持函数重载。但在C++中两者的函数名分别被修改了;
函数重载的底层实现-------参考材料:
C++函数的重载:
http://www.cnblogs.com/skynet/archive/2010/09/05/1818636.html
C/C++函数的调用约定:
http://blog.csdn.net/lioncolumn/article/details/10376891
总结:
C++支持重载是因为C++支持函数名修饰规则,即加入了参数;参数不同函数名修饰规则修饰出来就不同;编译完成以后,调用的函数可能是在其他目标文件中的;通过参数就可以找到想要调用的函数;
而C语言中去调用某个函数就会用该函数的名称去找,(如用.c文件中的一个函数去调用.h中的函数名,如果.h中有多个相同的该函数名就不能判断到底取哪个)即使参数不同函数名相同也无法区分谁是谁;
在多文件中去找函数名,不再是用原来的名字去找,而是用修饰的名字去找,当构成重载时说明是不一样的;此外还有可能在静态库或动态库中去寻找;
(静态库是程序编译后链接时调用的;动态库是程序运行时调用的;)
二.引用:
1.什么是引用:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间;
2.引用的格式:
类型& 引用变量名(对象名) = 引用实体
举个栗子:
#include <iostream>
using std::cout;
using std::endl;
int main(){
int a=10;
int &b=a; //b做a的别名,
a+=1;
b+=1;
std::cout << &a << std::endl;
std::cout << &b << std::endl;
return 0;
}
运行结果为:
a和b打印出来的地址是相同的,即它们用了同一块内存空间;
3.引用的特性:
(1)在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值;
(2)为引用提供的初始值可以是变量或另一个引用;
(3)指针是通过地址间接访问某个变量,而引用是通过别名直接访问某个变量,使用引用可以简化程序;
(4)引用在初始化后不能再重新声明为另一个变量的引用;
(5)可以将引用的地址赋给一个指针,此时指针指向原来的变量;
(6)引用仅在声明时带有引用运算符“&”,以后像普通变量一样使用,不能再带“&”,其它场合使用的“&”都是地址操作符;
4.引用的使用场景:
(1)引用作为参数:
#include <iostream>
namespace std{
void Swap(int &left,int &right){
int tmp=left;
left=right;
right=tmp;
}
}
using std::cout;
using std::endl;
int main(){
int a=10;
int b=20;
Swap(a,b);
cout<<a<<endl;
cout<<b<<endl;
return 0;
}
结果如下:
(2)做返回值:
# include <iostream>
namespace std{
int &Add(int a,int b){
int c=a+b;
return c;
}
using std::cout;
using std::endl;
int main(){
int& ret=Add(1,2);
Add(4,4);
cout<<"Add(1,2) is:"<<endl;
return 0;
}
结果如下:
为什么结果不是3,而是8??-----ret的返回值很受Add (4,4)这一行的影响;C是生成的变量的别名,ret也是生成变量的别名,也就意味着ret就是C的别名;此时C已经销毁,所以ret访问C就很危险;
引用做返回值有很大的缺点;
我们之前直到传值返回时,并不是把C给ret,而是在中间生成了一个临时变量(创建了空间),把C拷贝给了临时变量,然后把临时变量给了ret;
而传引用返回也是相当于生成了一个临时变量,但临时变量的类型是int&(传引用),没有创建空间只是生成了一个C的别名;
5.引用和指针的区别:
(1)在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间;
int main() {
int a = 10;
int& ra = a;
cout<<"&a = "<< &a<<endl;
cout<<"&ra = "<< &ra<<endl;
return 0;
}
结果为:(2)在底层实现上实际是有空间的,因为引用是按照指针方式来实现的;
int main() {
int a = 10;
int& ra = a; //语法上:ra是a的别名,没有开辟空间;
ra = 20;
int* pa = &a; //语法上:p开辟了4/8个字节的空间存储a的地址;
*pa = 20;
return 0;
}
//运行结果都为20 ;
指针和引用在语法上是两个东西,指针是开辟空间,引用是生成别名不开空间,但在底层实现上都是存地址的方式来实现的;
我们通过汇编代码进行对比:
关于引用和指针的不同点:
(1) 引用在定义时必须初始化,指针没有要求;
(2) 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
(3) 没有NULL引用,但有NULL指针;
(4) 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4 个字节);
(5) 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
(6) 有多级指针,但是没有多级引用 ;
(7) 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
(8) 引用比指针使用起来相对更安全;
三.指针空值:nullptr
我们先看一段代码:
int main(){
int *p=0;
int *p=NULL;
return 0;
程序中p1和p2本质上没有什么区别,可以理解为NULL是一个宏,这个宏被定义成了0;
再来看一段代码:
#include <iostream>
namespace std{
void Test(int ){
std::cout<<"Test(int)"<<std::endl;
}
void Test(int *){
std:: cout<<"Test(int *)"<<std::endl;
}
}
int main(){
Test(0);
Test(NULL);
Test((int *(NULL));
Test(nullptr);
return 0;
}
程序输出结果:
因此可以说明:NULL是一个int整型,并不是一个空指针;nullptr是一个void*型,是一个指针类型;
在C++11中定义一个空指针不会再用NULL,而是用nullptr;
空指针是一个存在的地址;
使用nullptr的注意事项:
注意:
(1) 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
(2) 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
(3) 为了提高代码的健壮性,在后续表示指针空值时建议好使用nullptr。