一、前言
在上一期的博客中,提到了C++较C语言新增的许多语法。在本次博客中,将会继续介绍更多有趣实用的C++语法,为之后的学习打下坚实基础。
二、引用
引用不是新定义一个变量,而是给一个已经存在的变量取别名。并且编译器不会给这个别名额外开辟新的内存空间,它和它所引用的变量将共享同一块内存空间。
打个比方,梁山泊一百单八个好汉,都有着各种各样的外号,比如宋江,江湖人称“及时雨”,有时又被叫做“呼保义”,但我们都知道这两个外号都指的是宋江这一个人,不会因为多出两个外号而额外多出两个人。
(一)引用的定义
类型& 引用变量名(对象名) = 引用实体
需要特别注意的是,引用实体和引用类型必须是同种类型!
从上图中就看出来类型不匹配,编译器就会报错。
void Test()
{
int a = 10;
int& a1 = a; //引用类型和引用实体是同一类型
printf("%p\n", &a);
printf("%p\n", &a1);
}
通过上面的代码和演示结果可以明显看出,变量a和引用变量a1共享同一块内存空间。
(二)引用的特性
1.引用在定义时必须初始化
引用必须要要有可以引用的对象。给一个变量起别名,那么就必须有这个变量的存在。
void Test()
{
int a = 10;
int& a1; //编译时会报错
}
2.一个变量可以有多个引用
正如前面所讲的,一个人可以有多个外号,一个变量也可以有多个引用。
void Test()
{
int a = 10;
int& a1 = a;
int& a2 = a;
printf("%p\n", &a);
printf("%p\n", &a1);
printf("%p\n", &a2);
//a、a1和a2的地址都相同,证明a1和a2都是a的引用
}
3.引用一旦引用一个实体,就再也不能引用其它实体
从上图中很容易看出,a1作为a的引用变量和a共享内存空间,a2则是另一个变量,开辟的是新的内存空间。即使将a2赋值给a1,也只是进行了单纯的赋值操作,a1和a2的内存空间并没有共享。
(三)常引用
用const修饰的引用,我们称之为常引用。
在代码中,如果需要给带const修饰的变量进行引用的话,那么常引用就可以派上用场了。
常引用也可以帮我们解决前面提到过的类型转换问题。
那么这是为什么呢?主要是因为在变量类型不同时赋值,编译器会进行隐式类型转换,在这个过程中会生成临时拷贝的变量。以上面的代码为例,a在这个过程中被拷贝到一个临时的变量中,而这个临时拷贝生成的变量具有常性。如果直接引用的话,会导致权限的放大,编译器就会报错。若加上const修饰的话,就是权限的平移,可以实现类型转换。
常引用可以引用普通变量和const修饰的变量,被称之为权限的缩小和平移。普通引用不能引用const修饰的变量,说明权限不能放大。
切记:权限不能放大,只能平移或缩小!
(四)引用的使用
1.做参数
引用可以作为函数的参数来使用。在函数中,函数的参数都是形参,而形参的改变是无法影响实参的。但使用引用的话,就可以直接改变实参,因为引用直接作用于引用实体(实参),形参在函数内部的修改,出了函数作用域仍然可以影响到实参。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 1;
int b = 2;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << endl;
Swap(a, b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
2.做返回值
引用也可以应用在函数返回值中。
但下面这种情况的结果却令人出乎意料。
在上面的代码中我们可以看到,明明在对引用变量ret赋值后并未做任何修改,打印处理的结果却大相径庭。那么造成这种情况出现的原因是什么?
在程序运行中,内存中会开辟一块空间,用于建立相关的函数栈帧。Add函数第一次调用时,返回值c的结果保存在栈帧之中。在调用结束后,函数栈帧被销毁,内存空间被释放,即失去了访问该内存空间的权限。所以main函数中引用变量ret所引用的是一块已经释放的内存空间,而第一次调用的返回值仍然在内存空间中,所以打印出来的结果是3。
在第二次调用中,再次建立栈帧,访问该空间,将函数运算的结果修改为7。在空间再次被释放后,第二次打印中ret所引用的空间存放的是7,所以打印出来的结果是7。
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回!
(五)传值和传引用的效率比较
传值有传值传参和传值返回。传值的特点是在传参和返回的过程中,不会直接作用于实参或返回值,而是在这个过程中生成一份临时拷贝来完成传参和返回的过程。因此传值的效率显而易见的低下,特别是在数据类型非常大的时候。
传引用就是引用的使用。由于使用引用就相当直接对引用实体进行操作,所以传引用的效率高。
1.传参
#include <time.h>
struct A { int a[10000]; };
void Test1(A a) {}
void Test2(A& a) {}
void Testtime()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
Test1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
Test2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "Test1(A):" << end1 - begin1 << endl;
cout << "Test2(A&):" << end2 - begin2 << endl;
}
int main()
{
Testtime();
return 0;
}
2.返回
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A Test1()
{
return a;
}
// 引用返回
A& Test2()
{
return a;
}
void Test()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
Test1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
Test2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "Test1:" << end1 - begin1 << endl;
cout << "Test2:" << end2 - begin2 << endl;
}
int main()
{
Test();
return 0;
}
(六)引用和指针的区别
引用在很多地方的作用与指针相同,比如说函数传值,指针用法是在形参列表中传实参的地址,来达到影响实参的效果。而引用则是直接作用于引用实体来进行操作。在语法层面上,二者并无交集,但在底层实现中,引用是用指针来实现,它是有一块空间的。
通过反汇编可以看出二者的底层实现方式是一样的。
引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求。
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体。
4. 没有NULL引用,但有NULL指针。
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。(32
位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
7. 有多级指针,但是没有多级引用。
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
9. 引用比指针使用起来相对更安全。
三、内联函数
(一)内联函数的定义
在C++中。以inline修饰的函数被称为内联函数,编译器在编译时会在调用内联函数的地方展开,减少栈帧建立的开销,提高程序运行的效率。
在功能上与C语言中的宏相同,但效果更好,可以替代宏。在C语言阶段,宏可以增强代码的复用性,提高性能,这些是宏的优点。然而宏的缺点也相当明显:因为在预编译阶段进行了宏替换,所以不方便调试宏,而且代码的可读性差,容易误用,并且宏没有类型安全检查,还容易犯语法错误,所以在C++中,我们常用内联函数来替代宏。
图中的汇编代码可以看出,程序运行中有调用call指令建立函数栈帧。
加上inline修饰后,无需call指令建立栈帧,函数直接在调用的地方展开,进行相加的操作。
(二)内联函数的特性
1.使用内联函数是一种以空间换时间的方法,在程序的编译阶段,会直接用函数体替换函数调用。好处是减少调用开销,提高运行效率;坏处是整个函数展开使得目标文件变大。
2.不同的编译器对inline的处理方式可能不同。内联函数对于编译器而言只是一个建议,编译器也完全可以忽视这个建议。一般建议用inline修饰函数规模小、非递归且频繁调用的函数。
3.内联函数不建议声明和定义分离,容易导致链接错误。当内联函数展开时,是没有函数地址的,链接就会找不到。以下是简单示例。
四、 auto关键字
在之后的深入学习中,程序会越来越复杂,类型也会越来越复杂,主要体现为两个方面:1.类型难于拼写。2.含义不明确导致容易出错。
简单举个例子,std::map<std::string, std::string>::iterator 是一个类型。这个类型的长度就完美符合以上两个特点,难写又难看。在C语言中,也许可以用typedef来给类型取给别名,但也容易遇得到以下困难。
在编写代码时,常常需要将表达式的值赋给变量,这就要求在声明变量时要明确知道表达式类型,因此auto的作用就体现出来了。
(一)auto的简介
在早期的C++中,auto修饰的变量是具有自动存储器的局部变量。现在的C++11新标准中,auto是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时推导得到。
注意:auoto定义变量时必须初始化!
由上面的代码可得,auto可以自动帮我们推导出类型。
(二)auto的使用
1.auto与指针与引用的结合
auto在声明指针类型时,auto和auto*并没有多大区别,但auto在声明引用时必须使用auto&
2.在同一行定义多个变量
在同一行声明多个变量时,这些变量必须是同类型的变量,否则编译器就会报错。因为编译器实际上只对第一个变量进行推导,然后再通过推导结果定义其它变量。
void test()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // c和d类型不同,会导致编译报错
}
(三)auto不能推导的场景
1.无法作为函数的参数使用。
2.不能直接声明数组。
3.为了避免和C++98的auto关键字混淆,C++11只保留了auto作为类型指示符的用法。
五、基于范围的for循环
当我们需要遍历一个数组时,可以采用以下方法:
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
显然,对于一个有范围的集合,代码上所写的关于循环的范围是多余且易错的。因此在C++11新标准中引入了基于范围的for循环。格式如下:
for( 范围内用于迭代的变量 : 被迭代的范围 )
注意:与寻常的for循环类似,可以用continue结束本次循环,也可以用break跳出循环。
范围for循环的使用条件:
1.for循环迭代的范围必须是可以确定的。对于数组来说,就是数组的第一个元素和最后一个元素的范围;对于类来说,需要提供begin和end来确定循环迭代的范围。
2.迭代对象要实现++和==的操作。
六、指针空值nullptr
在C语言程序编写中,如果一个指针没有明确的指明对象,一般是赋予NULL值。
int* p = NULL;
NULL在传统的C头文件中,可能被定义为字面常量0,也可能被定义为无类型指针(void*)的常量。这种含糊的定义很容易导致出现以下问题 。
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但因为NULL被定义为常量0,导致最终结果与预期不符。如果引入nullptr则将解决这个问题。
注意:
1.在使用nullptr表示指针空值时,无需包含头文件。
2.在C++11中,sizeof(nullptr)和sizeof((void*)0)所占字节数相同。
3.为了提高代码的健壮性,在之后表示指针空值时最后使用nullptr。
七、结语
到此为止,C++的基本入门知识已经全部讲解完毕,如果对您有任何帮助的话,这将是我莫大的荣幸 。感谢观看,下期再见!