【C++】C++入门(下)
引用
概念与使用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
如何定义一个引用:类型&
引用名 = 要引用的实体。引用类型必须和引用实体的类型一致
int a = 0;
int& b = a;
如上代码,b
就是a
的引用, 相当于给a
取了一个别名,a
和b
共用一块空间
我们可以取出这两个变量的地址看一下
int main()
{
int a = 0;
int& b = a;
cout << &a << endl;
cout << &b << endl;
return 0;
}
特性
1.一个变量可以有多个引用
一个变量也可以有很多别名,别名也可以有别名
就像一个人可以有很多名字一样,比如李逵,别名铁牛,铁牛和李逵的别名是黑旋风
int main()
{
int a = 0;
int& b = a;
int& c = a;
int& d = c; // 引用也可以有引用
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
a++;
b++;
c++;
d++;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}
2.必须初始化
在定义一个引用时,必须对其进行初始化,不能像下面这样定义引用
// 错误示例
int main()
{
int a = 0;
int& b;
b = a;
return 0;
}
运行上面的这个案例会发生错误:
所以,定义引用的同时必须初始化
// 正确示例
int main()
{
int a = 0;
int& b = a;
return 0;
}
3.不能更改指向
一个引用一旦定义,他就不能成为别的实体的别名了
int main()
{
int a = 0;
int b = 1;
int& c = a; // c 是 a 的引用
c = b; // 赋值
return 0;
}
如上代码,c
已经是a
的引用了,无法再更改指向。c = b
也只是将b
的值赋给c
其实这一点和必须初始化相呼应
还是来看下面这个错误示例
// 错误示例
int main()
{
int a = 0;
int& b;
b = a;
return 0;
}
b = a
是要把a
的值赋给b
呢,还是b
要作为a
的别名呢?这就会有歧义
常引用
我们知道,被 const
修饰的变量,只能查看,不可修改
当一个变量被const
修饰时,其引用也要加const
修饰,这就是常引用。如下
int main()
{
const int a = 0; // a 被const修饰
const int& ra = a; // 引用也要加const
return 0;
}
可以这样理解,引用实体被const
修饰时,只有只读权限;如果引用不加const
,权限就是可写可读,造成了权限的放大,这是不行的
如下就是权限放大,会报错
int main()
{
const int a = 0; // 只读
int& ra = a; // 可写可读,权限放大
return 0;
}
对应地,也会有权限的缩小。
权限缩小是指:引用实体可写可读,而其引用只读。权限的缩小是被允许的,如下
int main()
{
int a = 0; // 可写可读
const int& ra = a; // 只读,权限缩小
cout << "a:" << a << endl;
cout << "ra:" << ra << endl;
return 0;
}
引用的应用场景
1.作参数
引用作为参数通常都是输出型参数。输出型参数是指:形参改变,实参也会改变
例如,写一个函数来交换两个变量的值。在学习引用之前,我们通常会用指针来写
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 1;
int b = 2;
cout << "a:" << a << " b:" << b << endl;
Swap(&a, &b);
cout << "a:" << a << " b:" << b << endl;
return 0;
}
那么学习了引用后,我们那就可以使用引用作为参数,形参的改变也会影响实参
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 1;
int b = 2;
cout << "a:" << a << " b:" << b << endl;
Swap(a, b);
cout << "a:" << a << " b:" << b << endl;
return 0;
}
作为输出型参数这个功能,指针和引用两者都可以实现,但是引用使用起来更加便捷
另外,当参数很大时,使用引用也可以避免拷贝,从而提高运行效率
2.作返回值
我们先来看一般的整型作返回值
int func()
{
int a = 1;
return a;
}
int main()
{
int ret = func();
return 0;
}
调用func
后,ret
的值:
func
中的a
是局部变量,函数结束后生命周期就结束了,会销毁,为什么还会给返回给ret
呢?
其实在函数销毁前,会在函数栈帧之外形成一份a
的临时拷贝,返回的是a
的拷贝,所以函数结束后,依然能赋值给ret
这时我们对代码稍作修改,把函数的返回值类型改为引用
int& func()
{
int a = 1;
return a;
}
int main()
{
int ret = func();
return 0;
}
此时,这份代码就有问题了:
func
返回的是a的别名,就相当于a所对应的空间已经销毁了,但是func
结束后还是把那块空间的数据返回,给了ret
此时ret
的值是不确定的,如果编译器没有对那块空间进行清理,那就还是a
的值;如果清理了,就是随机值了
再接着修改,将ret
改为引用
int& func()
{
int a = 1;
return a;
}
int main()
{
int &ret = func();
return 0;
}
此时的代码问题就更大了,func
返回的是a
的别名,而ret
是引用,也即是说,ret是a的引用
a
对应的空间已经销毁,但是ret
作为a
的引用,还是能访问到这块空间。与野指针类似,这里是野引用
综上所述,引用不能返回局部变量,因为局部变量除了作用域就会销毁,再用引用访问就是非法的,上面所说的都是引用作返回值的错误示例
引用不能返回局部变量,但是可以返回全局变量、静态变量、堆上变量,下面我们就来看一下引用作为返回值的正确用法
如果我们要实现一个顺序表,取出某个位置的数据和修改某个位置的数据,是要分别实现两个接口的,如下:
// 顺序表结构
typedef struct SList
{
int* data;
int size;
int capacity;
}SList;
// 初始化
void SLInit(SList* ps)
{
ps->data = (int*)malloc(sizeof(int) * 4);
ps->capacity = 4;
ps->size = 0;
}
// 尾插
void SLPushBack(SList* ps, int x)
{
ps->data[ps->size] = x;
ps->size++;
}
// 取得pos位置的值
int SLGet(SList* ps, int pos)
{
return ps->data[pos];
}
// 修改pos位置的值
void SLModify(SList* ps, int pos, int x)
{
ps->data[pos] = x;
}
现在我们要查看数据,修改数据,要调用两个接口
int main()
{
SList sl;
SLInit(&sl);
// 尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
// 遍历查看
for (int i = 0; i < 4; i++)
{
// 调用 SLGet()
cout << SLGet(&sl, i) << " ";
}
cout << endl;
}
再把偶数位乘二
int main()
{
SList sl;
SLInit(&sl);
// 尾插
SLPushBack(&sl, 1);
SLPushBack(&sl, 2);
SLPushBack(&sl, 3);
SLPushBack(&sl, 4);
// 遍历查看
for (int i = 0; i < 4; i++)
{
cout << SLGet(&sl, i) << " ";
}
cout << endl;
// 偶数位乘二
for (int i = 0; i < 4; i++)
{
int val = SLGet(&sl, i);
if (val % 2 == 0)
{
SLModify(&sl, i, val * 2);
}
}
// 遍历查看
for (int i = 0; i < 4; i++)
{
cout << SLGet(&sl, i) << " ";
}
cout << endl;
}
通过引用作返回值,就可以将SLGet
和SLModify
的功能合并为一个接口,只需将SLGet
的返回改为引用即可
int& SLGet(SList* ps, int pos)
{
return ps->data[pos];
}
返回的是引用,也就是说,返回的是pos位置的别名
我们可以通过引用来获取pos位置的值,也可以对pos位置的值作出修改
修改后的测试代码
// 遍历查看
for (int i = 0; i < 4; i++)
{
cout << SLGet(&sl, i) << " ";
}
cout << endl;
// 偶数位乘二
for (int i = 0; i < 4; i++)
{
if (SLGet(&sl,i) % 2 == 0)
{
// 直接就能修改
SLGet(&sl, i) *= 2;
}
}
// 遍历查看
for (int i = 0; i < 4; i++)
{
cout << SLGet(&sl, i) << " ";
}
cout << endl;
引用和指针的关系、区别
关系
现在我们已经对引用有了初步了解,也可以发现引用和指针是类似的,作用是有重叠的
那么引用可不可以替代指针呢?——不可以。指针和引用虽然相似,但不可以相互替代
比如,链表的中的指针,链表可以通过修改指针来修改各个节点的指向关系。而引用是不能改变指向的,自然也替代不了指针
总的来说,虽然引用使用起来更加便利,但是不能替代指针;引用与指针是相辅相成的
区别
语法上
- 引用是给变量取别名,不需要开空间;而指针是变量的地址,需要开空间
- 引用必须初始化;指针可以不初始化
- 引用不可以改变指向;指针可以改变指向
- 引用没有空引用,野引用也不容易出现;指针有空指针,且空指针和野指针都容易出现、
- sizeof含义不同:引用为引用类型的大小;指针为4或8个字节
- ++含义不同:引用是引用的实体加一,而指针是地址偏移一个类型的大小
底层上
引用在语法上是没有开空间的,在底层上是有开空间的,即引用的语法含义与底层实现是背离的
为什么呢?因为在底层上,引用是用指针实现的,在底层只有指针,没有引用
我们可以来验证一下,有以下代码
int main()
{
int a = 0;
// 引用
int& ra = a;
// 指针
int* pa = &a;
return 0;
}
使用VS进入调试模式,再转到反汇编
以上就是汇编代码,先不管能不能看懂,至少我们可以看到引用和指针的操作都是一样的
这就说明,在汇编层面,是没有引用的,引用是用指针实现的,所以引用在底层也会开空间
内联函数
在C语言阶段,我们有时会用宏定义一些简单的函数,以此来增强代码的复用性,提高性能
例如使用宏定义一个加法函数
#define ADD(a,b) ((a) + (b))
int main()
{
int a = 1;
int b = 1;
int ret = ADD(a, b);
cout << "ret:" << ret << endl;
}
但是宏有很多缺点:
- 语法复杂,使用起来容易出错
- 预编译阶段会进行宏替换,不容易调试
- 不会进行类型检查
概念及使用
而在C++中,我们可以使用内联函数实现类似宏函数的功能,调用内联函数不会创建栈帧,从而减少损耗
只需在函数前加inline
关键字修饰,就可以将其改为内联函数
inline int Add(int x, int y)
{
return x + y;
}
调用内联函数时不会创建栈帧,而是在调用处展开。不同于宏,内联不是将代码替换到调用处,而是以灵活的方式将函数的逻辑在调用处展开
我们可以将普通函数和内联函数对比
// 普通函数
int Add(int x, int y)
{
return x + y;
}
查看此函数的汇编层
当call
时,就会发生跳转,创建函数栈帧
再来看看内联函数是怎样的
// 内联函数
inline int Add(int x, int y)
{
return x + y;
}
在debug模式下,需要对编译器进行设置,以下是 VS2019 的设置方法
右击项目
点击属性
常规->调试信息格式->程序数据库
再点击 优化->内联函数扩展->只适用于_inline
然后就可以进入调试模式,转到汇编层面了
可以看到,调用内联函数时并没有call
,而是将函数逻辑展开了
特性
1.从上面可以看出,内联函数不会建立函数栈帧,但是会在调用处展开;这样可以减少调用开销,提升程序运行效率,但是会使文件体积增大。这是一种以空间换取时间的做法
2.也不是任何函数都可以成为内联函数。一般建议:将函数规模小、调用频繁、不是递归、的函数用inline
修饰
3.内联函数的声明与定义不可分离。现有如下代码
// .h文件
inline void func(int i);
// .cpp文件
void func(int i)
{
cout << i << endl;
}
// test.cpp文件
int main()
{
func(10);
return 0;
}
由于内联函数不会调用,是直接展开的,那就没有call
,也就不会有函数地址,在链接阶段就会找不到
auto关键字(C++11)
auto关键字可以自动识别变量的类型,如下
int a = 0;
auto aa = 0;
char b = 'a';
auto ab = 'a';
double c = 1.1;
auto ac = 1.1;
我们可以使用typeid(变量).name()
查看变量的类型
cout << typeid(a).name() << endl;
cout << typeid(aa).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(ab).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(ac).name() << endl;
用法
1.必须初始化
下面这种用法是会报错的
auto a;
a = 10;
2.可以搭配引用、指针使用
int main()
{
int a = 0;
// 指针
auto pa1 = &a;
auto* pa2 = &a;
// 引用
auto& ra = a;
cout << typeid(pa1).name() << endl;
cout << typeid(pa2).name() << endl;
cout << typeid(ra).name() << endl;
}
注意:如果左边是auto*
,那么右边必须给地址;引用必须是auto&
3.一行可以定义多个变量,但是一定得是同一类型
auto a = 1, b = 2; //符合规范
auto c = 3, d = 4.0; //不符合规范,会报错
auto不能使用的场景
1.auto
不能在函数形参中使用
void TestAuto(auto a)
{
cout << a << endl;
}
int main()
{
TestAuto(10);
return 0;
}
2.不能直接用来声明数组
int main()
{
int arr[] = { 1,2,3 };
auto arr2[] = { 4,5,6 };
return 0;
}
基于范围的for循环
在C语言中,要是想遍历一个数组,我们一般是这样:
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
{
cout << array[i] << " ";
}
cout << endl;
}
而在C++中,对于有范围的集合,我们一般是这样:for循环后的括号由冒号“ :”分为两部分:第一部分是范
围内用于迭代的变量,第二部分则表示被迭代的范围
将上面的代码改造一下:
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto i : array)
{
cout << i << " ";
}
cout << endl;
}
意思是将数组中的元素依次赋给变量i
,自动迭代,自动判断结束。auto 也可以写成数组中元素的类型,但是一般都写auto
如果想修改数组中的元素,可以在auto
后加个&
,将i
写成引用
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto& i : array)
{
i *= 2;
cout << i << " ";
}
cout << endl;
}
指针空值(C++11)
在C++98中,指针空值的定义有一些问题,先看以下代码
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
}
我们期望的是f(0)
调用f(int)
,f(NULL)
调用f(int*)
,代码也理应该是这样
但是运行结果确是这样:
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器
默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void
*)0。
所以,C++11引入了新关键字:nullptr
来表示指针空值
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(nullptr);
}
这样就符合程序的初衷了
注意:nullptr
是个关键字,不需要包含头文件了;为了提高代码健壮性,建议使用nullptr
结束,再见 😄