
🔥个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题、C/C++干货分享&学习过程记录 🍉学习方向:C/C++方向 ⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平

前言:本专栏记录了博主C++从初阶到高阶完整的学习历程,会发布一些博主学习的感悟、碰到的问题、重要的知识点,和大家一起探索C++这门程序语言的奥秘。本文作为本专栏的第三篇文章,也是C++入门的最后一篇文章,起到了奠定基调的作用。这个专栏将记录博主C++语法、高阶数据结构、STL的学习过程,正所谓“万丈高楼平地起”,我们话不多说,继续进行C++阶段的学习。
C++的两个参考文档:
老朋友(非官方文档):cplusplus 官方文档(同步更新):cppreference
表达层(即语法层,用一种理论形容功能)和底层(实现层)有时候可能是不一样的,这里提醒一下看到本文的友友们:面试时假如面试官问到了引用,我们就讨论语法层,千万不要扯到底层(实现层),我们就简单从语法层面看待就好了。
至于细节,上篇文章博主已经解释过了,在本文开头的回顾部分就不多赘言了,博主这里把上篇文章的链接放在下面了,感兴趣或者忘记了的友友们可以去看看——
【C/C++】初识C++(二):深入详解缺省参数(默认参数)函数重载、引用(重头戏)
如果返回值是局部对象,出了作用域就销毁了,就很危险,相当于访问野指针。
下面的是一个类似于伪代码的东西——

注意:传值返回返回的是拷贝,引用返回返回的就是引用(别名)。
出了Func函数作用域,ret如果还在,就可以用引用返回——全局、静态或者出了Func函数作用域还在的变量就可以用引用返回。
对比指针的传值返回,传参、传引用返回(减少拷贝)。 传值传参、深拷贝(代价会更大)中,引用和指针的对比会更明显(4/8个字节),如果是比较大的对象,几百字节的那种就明显了。引用始终都是一个指针的开销(对任何对象)——引用传参,指针和引用功能重叠,引用会更方便,不用取地址。

引用不需要像指针那样需要解引用(当然指针也可以用)。 C++引用不能改变指针指向,引用和指针相辅相成,不存在相互替代。 除了像链表、树这些指针无法被替代的数据结构,其他我们都开始用引用了。
函数的地址是第一个指令的地址(看汇编)。
1、可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大; 2、不需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样一些场景下a*3的和结果保存在一个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以; 3、所谓临时对象,就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。

权限可以改变,但是权限不能放大——

const修饰a,a自己都不能改变,b(引用)却能改变a,这是不可以的。
权限不能放大,可以这样写,权限平移——

这里不是权限放大,是拷贝复制——

不是权限放大,是拷贝复制,e(新空间)的改变不会影响a,而b(引用)的改变会影响a:

权限可以缩小——

d作为c的别名,权限缩小了,d只能读不能写。

1、权限不能放大; 2、权限可以平移; 3、可以权限缩小。
权限的缩小放大,const指针 / 引用(只存在这两种情况)——


const int* p2 = p1(权限平移)。
可以权限缩小——

(1)减少拷贝; (2)形参改变影响实参,要加const,否则会有很多限制—— 如果不是像Swap这种改变形参要影响实参的情况,都建议用const修饰。
注意:
C++链表尾插——
std::list::push_back也是带const的。
隐式类型转换(C语言支持),还有显式类型转换、强制类型转换。
整型---整型,整型---浮点型,浮点型---浮点型,类型之间的转换一定要有关联。

本质上是权限放大,要用const修饰。
传值返回(小的存寄存器,大的存栈帧空间中间——临时对象——这是一种语法设计,完了销毁)。

这个奇怪的语法是为传参准备的(引用本来就是为了传参、传引用返回——引用真正的应用),理解语法不要扯到地址,两个层面放一块儿会加大理解难度。
const修饰主要是用在参数,也有用到返回值的。
C++中指针和引用就像是两个性格迥异的亲兄弟,指针是哥哥,而引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各自有自己的特点,互相之间是不可替代的。
上篇文章我们已经介绍了在链表、树里面不能用引用替代指针,因为引用不能改变指针指向。因此,指针和引用是相辅相成,互相不可替代的。
下面我们梳理一下指针和引用之间的关系: 大家注意,这个关系不要背,要理解,面试的时候如果问到了能说个大概就行,C++这种东西太多了背不完的——

引用和指针的关系非常重要,说漏一两点不重要的不要紧,只要让面试官知道你熟悉这块就行。
看到标题,大家就应该知道了,就像引用是用来替换C语言复杂的指针一样,inline(修饰内联函数)是用来替换C语言中不靠谱的宏。
(1) 宏只是像传参,但它是一种替换机制; (2) 宏函数不要加“;”(替换机制); (3) 宏函数的重点不在后面的函数。
宏的坑很多:宏的常量、宏函数——这些我们现在都用inline替代。
C++建议:const enum(枚举) inline(替换宏)。
宏真的有点复杂!
假如面试的时候,面试官问你: “小伙子(或者小姑娘),你简历上面写着【精通/掌握C++】,我非常感兴趣,正好考考你——写一下ADD的宏函数吧!” ——哈哈哈,千万不要把精通、掌握这种词往简历上面写啊,会被整得很惨的。

——这里其实也就解释了为什么“;”不能乱加的问题。
博主直接展示一下ADD的宏函数试错的代码截图——

说了宏一堆坏话,但宏不是一无是处的,否则我们也不会用它了,早就被删除了。
宏的缺陷:宏函数很复杂,容易写出问题,而且宏还不能调试(预处理阶段)。 宏的优点:高频调用小函数,写成宏函数,可以提高效率,预处理阶段宏会替换,提高效率,也是在预处理阶段,又是小函数,不建立函数栈帧,而且宏可以传类型。
因此宏是把双刃剑,尽管它有那么多缺点,但还是有人在用它,就是因为它的优点。
内联就不是宏那个替换机制了——

加上inline就变成内联函数了。
C++在调用(编译)时会展开内联函数(我们让编译器多处理一下,没有函数调用,也不建立函数栈帧),这样可以提高效率。
变不变内联函数程序员说了不算,编译器说了才算。
因为inline对于编译器只是一个建议,针对短小的函数(VS的这个“短小”具体的边界是10行代码),对于像递归这样的代码相对多一些的编译器就会选择忽略inline。
这个不展开inline具体的边界是多少具体还是得看编译器。
补充:
(1)编译器默认debug版本下,为了方便调试,inline也不展开(我们去反汇编看) (2)release版本会展开,但是release版本一不能调试,二不能看汇编。
下面是我们在观察inline修饰内联函数是否展开时对VS编译器的修改——


备注:对于汇编的小bug,我们可以点击【生成解决方案】清理一下——

编译器自己在把握,inline是否展开是有边界的(VS是10行左右算短小函数)。
inline不会展开,还是会建立函数栈帧,inline被编译器忽略。
——如果随便交给程序员可能会有“恶性膨胀”的问题。
inline Func函数,展开后是20行代码,如果有10000个调用的位置——一行变20行!
这个差距会很大——

内联函数的缺陷: 代码指令膨胀,可执行程序(安装包)变大了——变太大了就不能接受了。
因为怕程序员不靠谱,编译器自己把控,防止造成安装包内存庞大的恶果。
我们去公司——尤其是去一些有一定年头的互联网公司上班的时候,可能会面对几年前甚至几十年前的老代码、老语法,这些底层的代码、老的框架虽然说很老,但是很稳定。公司也不会说专门去让人维护一下这些又老又稳定的程序,因为公司最贵的成本就是人力。 这些老框架、老代码里面有宏。
因为inline指令展不开,要展开函数的实现,这种内联编译器很为难,找不到,没法展开。会报一个链接错误的错误提示(编译链接阶段找不到这个函数了)。我们包了.h文件,.h文件预处理阶段展开。
定义生成函数的地址,内联函数不放地址,不放符号表,所以出错。
总之内联函数直接在.h文件定义,不要让声明(.h)和定义(.cpp)分离——

简单理解就是语法就是这么规定的,防止出问题的。
inline被展开,就没有函数的地址,编译会报错。 解决方案:直接在.h定义。
补充: 函数的地址是第一个指令的地址。
nullptr(C++11的关键字),是为了代替NULL(C的缺陷)。
NULL实际是一个宏,在传统的C头文件(stddef.h)中,如下图所示——


像下面这种——
(void*)0就不能直接给int,要强制类型转换。
C、C++对NULL都有坑,所以,C++11创造出了nullptr——是特殊类型的字面类,可以转任何类型的指针类型, nullptr值为0。 ——隐式类型转换。
我们用nullptr替换NULL,解决NULL的一些转换相关的问题。
以后我们就都用nullptr了!
#pragma once
#include<stdlib.h>
typedef struct SeqList
{
int* a;
int size;
int capacity;
}SL;
inline void SLInit(SL* pls, int n = 4)
{
pls->a = (int*)malloc(sizeof(int) * n);
pls->size = 0;
pls->capacity = 0;
}
void SLPushBack(SL* pls, int x);
int SLFind(SL* pls, int x, int i = 0);
int& SLat(SL* pls, int i);
void SLModify(SL* pls, int i, int x);#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"
//void SLInit(SL* pls, int n)
//{
// pls->a = (int*)malloc(sizeof(int) * n);
// pls->size = 0;
// pls->capacity = n;
//}
void SLPushBack(SL* pls, int x)
{
//...
pls->a[pls->size++] = x;
}
int SLFind(SL* pls, int x, int i)
{
while (i < pls->size)
{
//...
}
return -1;
}
int& SLat(SL* pls, int i)
{
//...
return pls->a[i];
}
void SLModify(SL* pls, int i, int x)
{
//...
pls->a[i] = x;
}#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//const引用
int main()
{
const int a = 0;
////权限不能放大
//int& b = a;
//不是权限放大,是拷贝复制
int e = a;
const int& b = a;
int c = 0;
//权限可以缩小
const int& d = c;
//权限放大缩小,const 指针&(或者)引用
const int* p1 = &a;
////不能权限放大
//int* p2 = p1;
const int* p2 = p1;
//可以权限缩小
int* p3 = &e;
const int* p4 = p3;
return 0;
}
void func(const int&x)
{ }
int main()
{
const int& a = 10;
int y = 0;
func(y);
const int z = 1;
func(z);
func(2);
double d = 2.2;
func(d);
return 0;
}
int main()
{
int i = 0;
double d = i;
int p = (int)&i;//强制类型转换
const double& rd = i;
const int& rp = (int)&i;
return 0;
}
//C语言的宏坑很多
//C++建议 const enum(枚举) inline(内联函数)替代宏
//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 ret1 = ADD(1, 2);//相当于 int ret1 = 1 + 2;;
cout << ret1 << endl;
int ret2 = ADD(1, 2) * 3;
cout << ret2 << endl;
//a和b是表达式,表达式中的运算符(下面是|或运算和&与运算)的优先级比+的优先级要低
int x = 0, y = 1;
ADD(x | y, x & y);//((x|y) + (x&y));
return 0;
}
//默认debug版本下,inline也不展开,目的为了方便调试
inline int Add(int a, int b)
{
a += 1;
a += 1;
a += 1;
a += 1;
a += 1;
a += 1;
a += 1;
return a + b;
}
int main()
{
int ret2 = Add(1, 2) * 3;
cout << ret2 << endl;
return 0;
}
#include"SeqList.h"
int main()
{
SL s;
SLInit(&s);// call 地址
return 0;
}
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
//f((void*)0);
f(nullptr);
int* p1 = NULL;
char* p2 = NULL;
int* p3 = nullptr;
int* p4 = nullptr;
return 0;
}【C/C++】初识C++(二):深入详解缺省参数(默认参数)函数重载、引用(重头戏)
【C/C++】初识C++(一):C++历史的简单回顾+命名空间、流插入、命名空间的指定访问、展开问题等概念整理
结语:本文内容到这里就全部结束了, 本文我们在上一篇文章的基础上,对C++的入门内容做了收尾。我们衔接前文有关C++引用的内容,继续学习了const引用,指针和引用关系梳理,inline(内联函数),nullptr替代NULL等知识点,从现在一直到学习到模版初阶学完之后,都是些晦涩的概念,还用不起来,到后面我们就能像之前那样,结合起来介绍。