文章目录
- 引用
- 内联函数
- auto关键字
- 基于范围的for循环
- 指针空值nullptr
- 后记
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
所谓引用就是给变量起别名,在语法上来说是不会开辟变量的。
例如:
int main()
{
int a=0;
int&ra=a;
a=5;
cout<<a<<endl;
cout<<ra<<endl<<endl;
ra=10;
cout<<a<<endl;
cout<<ra<<endl;
}
如上,即为代码的运行结果。可以看到我们改变了a的值,ra也跟着改变。改变了ra的值,a也跟着改变。这个ra就类似于c语言的指针,但此处不同在于不需要解引用。
此外,引用还有以下特性:
- 引用在定义时必须初始化
这是很好理解的,如果不初始化就很容易有歧义:
int a=0;
int b=0;
int&ra;
ra=a;
ra=b;
如上,我们便没有对ra进行初始化,那么下面ra=a和ra=b,这到底是说ra是a的别名还是b的别名呢?所以,为了避免这样的歧义存在,引用在定义时必须初始化。
- 引用一旦引用一个实体,再不能引用其他实体。原因还是为了避免歧义。这也是和指针不同的一点,这也导致有些场景只能用指针而不能用引用来解决
- 一个变量可以有多个引用。
一个人可以有多个别名,那么变量当然也可以有多个别名。
除了上面的特性外,使用引用还有以下需要注意的点:
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
上面我们了解了引用的定义,那么c++多出这么个概念又有什么作用呢?
别急,下面我们就来谈谈引用的使用场景:
- 做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a=10,b=5;
Swap(a,b);
}
如上代码,我们以往使用c语言时传参就必须是穿址调用才能交换a,b的值。现在可以引用调用,只传a,b的值就能实现Swap。
这样做的一大好处就是增加了代码的可读性,不用时不时传一个指针、二级指针,不仅不好理解,传参的时候也麻烦。此外,由于引用实际上是给变量取别名,所以left和right实际上就是a和b,换句话说这两个变量不需要压栈,这就减少了空间开销。
不过话又说回来,引用的底层实际上还是用指针来实现的。
- 做返回值
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
这里的返回值是n的一个引用,我们就可以通过改变返回值来改变n的值。但是正如指针会有野指针,引用也有野引用。引用做返回值的时候,需要注意返回的对象没被销毁。因此只有堆、静态区、全局变量才能返回引用。
学习了引用,细心的读者也可以发现这简直和指针太像了,一股子指针味。这下谁还分得清什么是指针,什么是引用啊!无妨,接下来我们谈谈引用和指针的区别:
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用(给引用再引用,实际上也只是给原变量取多一个别名而已。)
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
阅读上面的指针和引用的区别,我们也了解到了引用的一些不足之处,但是总体来说,相较于指针,引用还是更方便的。
内联函数
学习内联函数前,我们要知道编译器调用函数时要进行申请栈帧、压栈、弹栈等操作的。
如果一段较为简短的代码在程序里反复调用,那么上述过程产生的开销就很大。因此在c++中专门用一个关键字inline修饰内敛函数,来达到类似宏定义函数的效果,从而解决上述问题。
如下:
inline int Add(int a=0,int b=0)
{
return a+b;
}
int main()
{
Add();
Add();
Add();
Add();
Add();
Add();
Add();
Add();
Add();
return 0;
}
如上函数,如果不加上inline修饰,那么Add调用消耗栈帧就比较大。现在用inline修饰后,Add这个函数会进行代码块展开,这用就不用进行开辟栈帧、压栈、弹栈等操作。
inline的特性:
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。
auto关键字
随着程序逐渐复杂,变量类型也逐渐复杂。就算是一个简单的函数int Add(int ,int ),其函数指针的类型为int (*)(int ,int ),这也是比较复杂的。虽然可以通过typedef来进行一定程度的改善,但是就算是使用typedef也是比较繁琐的。
为了解决上述的问题,c++使用auto关键字来自动识别变量类型。
如下:
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
上面代码的b,c,d都用了auto来自动识别类型,其中b为int,c为char,d为int。是不是感觉方便了不少呢?
需要注意的是,使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto的使用场景:
- auto与指针和引用结合起来使用,用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。(当然auto*声明的变量必须是指针类型,而auto声明的变量类型既可以是指针类型,也可以不是指针类型)
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译
器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto不能使用的场景:
- auto不能作为函数的参数
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};//不可行
}
基于范围的for循环
范围for循环:
一个已知范围的数组可以用如下的遍历方式:
int main()
{
int arr[]={1,2,3,4,5,6,7,8,9,10};
for(int i=0;i<sizeof(arr)/sizeof(int);i++)
arr[i]++;
return 0;
}
对于一个已知范围的数组,需要遍历其元素时用上述方式显得繁琐,而且还是错误的风险。
在c++中,我们就可以使用下面的方式来遍历数组元素:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return;
}
就这样,我们使用了一种较为简洁的方式来遍历有范围的数组。
范围for的使用条件:
- for循环迭代的范围必须是确定的。对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
上述代码就是一个典型的使用错误,因为函数压栈时是绝对不会传数组的,这样的空间开销太大。所以这里的array是一个指针,因此array的范围并不明确。
- 迭代的对象要实现++和==的操作。
指针空值nullptr
在C语言中我们常用NULL来赋值空指针,但实际上NULL是一个宏定义的值,本质是int 0.
前面我们学习了函数重载,由此就引出了一个问题。
void Test(int x)
{
cout << "int" << endl;
}
void Test(int* x)
{
cout << "int*" << endl;
}
int main()
{
Test(0);
Test(NULL);
return 0;
}
上述代码的理想的输出结果为:
int
int*
但实际的输出结果为:
也就是我们的NULL被识别成了int类型,这和我们的本意显然是冲突的。
由此,c++引入了指针空值nullptr。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
后记
以上就是大致的C和C++的一些用法上的不同,显然C++的各种语法改良了C语言很多不足的地方,这也为我们叩响了C++的敲门砖。就借着此处的优势继续向着C++的更深处进发吧。