目录
6.引用
6.1 引用的特性
6.2 const引用
7.指针和引用的关系
8.内联函数
9.nullptr
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。比如:NBA中的库里也被称为“小学生”,韦德外号“闪电侠”,奥尼尔,外号“大鲨鱼”。
类型& 引⽤别名 = 引⽤对象;
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的>,这⾥引⽤也和取
地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。
#include using namespace std; int main() { int a = 0; // 引⽤:b和c是a的别名 int& b = a; int& c = a; // 也可以给别名b取别名,d相当于还是a的别名 int& d = b; ++d; // 这⾥取地址我们看到是⼀样的 cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0; }
(1)引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象
void Swap(int& rx, int& ry) { int tmp = rx; rx = ry; ry = tmp; } int main() { int x = 0, y = 1; cout << x <<" " << y << endl; Swap(x, y); cout << x << " " << y << endl; return 0; }
以上这个程序就是对引用在对函数传参时效率提高的体现,通常我们如果要交换两个数的值,如果要写一个函数实现,我们必须将两个数的地址,由指针来交换它们的值,而我们在c++学了引用之后,我们也可以使用引用接收两个数的值来交换两个数的值。
(2)引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
(3)引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
(4)引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他 语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向, Java的引⽤可以改变指向。
(5)⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
(1)可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访 问权限在引⽤过程中可以缩⼩,但是不能放⼤。
从上图可以看出,我们使用const修饰了一个整型变量,而我们使用引用给它改名时出现了报错,这就是典型的权限放大。被const修饰的a是一个可读不可写的变量,当我们使用引用给它改名时,不加const修饰,从程序的角度来看,我们试图得到一个可读可写的变量,我们知道,引用本质上是对一个变量改名,所以b就是a,而a是一个可读不可写的变量,b也应该是应该可读不可写的变量,所以在这里程序出现了错误,纠错的方法也很简单,只需要用const修饰b就可以了:
加上const修饰后程序就没有错误了。
(2)不过需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
#include using namespace std; int main() { int a = 10; const int& ra = 30; // 编译报错: “初始化”: ⽆法从“int”转换为“int &” // int& rb = a * 3; const int& rb = a*3; double d = 12.34; // 编译报错:“初始化”: ⽆法从“double”转换为“int &” // int& rd = d; const int& rd = d; return 0; }
(3)所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功 能有重叠性,但是各有⾃⼰的特点,互相不可替代。
(1)语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
(2)引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
(3)引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
(4)引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
(5)sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
(6)指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
(1)⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴栈帧了,就可以提⾼效率。
#include using namespace std; inline int Add(int x, int y) { int ret = x + y; ret += 1; ret += 1; ret += 1; return ret; } i nt main() { // 可以通过汇编观察程序是否展开 // 有call Add语句就是没有展开,没有就是展开了 int ret = Add(1, 2); cout << Add(1, 2) * 5 << endl; return 0; }
(2)inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
(3)C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数。
#include using namespace std; // 实现⼀个ADD宏函数的常⻅问题 //#define ADD(int a, int b) return a + b; //#define ADD(a, b) a + b; //#define ADD(a, b) (a + b) // 正确的宏实现 #define ADD(a, b) ((a) + (b)) // 为什么不能加分号? // 为什么要加外⾯的括号? // 为什么要加⾥⾯的括号? int main() { int ret = ADD(1, 2); cout << ADD(1, 2) << endl; cout << ADD(1, 2)*5 << endl; int x = 1, y = 2; ADD(x & y, x | y); // -> (x&y+x|y) return 0; }
(4)inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
(1)C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种
定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的
f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);
调⽤会报错。
(2)C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
#include using namespace std; void f(int x) { cout << "f(int x)" << endl; } v oid f(int* ptr) { cout << "f(int* ptr)" << endl; } i nt main() { f(0); // 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。 f(NULL); f((int*)NULL); // 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型 // f((void*)NULL); f(nullptr); return 0; }
c++基础篇到这里就结束了,我们下期再见!