本期我们接着来讲C++的基础知识,没有看过的朋友可以先看看上一期
(16条消息) C++基础知识-----命名空间_KLZUQ的博客-CSDN博客
目录
4.缺省参数
5.函数重载
6.引用
7.内联函数
8.auto关键字(C++11)
9. 基于范围的for循环(C++11)
10.指针空值nullptr(C++11)
4.缺省参数
如果有多个缺省参数,我们也可以只传部分
我们传入的参数少于缺省参数,会从左往右依次传给缺省参数
另外,我们不能跳着传参
上面的缺省叫做全缺省,因为这些参数都是缺省参数,与之相对,还有半缺省
如果有一个参数不是缺省,那我们就必须至少传入一个参数
缺省是从右往左的,即我们的缺省参数要从右边开始,而不能左边是缺省,右边不是
这与上面传参相对应
我们来看一些缺省参数的应用
我们在C语言阶段定义栈时,我们当时默认给了栈的空间是4,如果一个人知道他的数据容量为100,那此时就要不断的扩容,非常麻烦,而我们这里给出缺省参数,就可以根据不同的需求来给定空间了,而一个人不知道他的数据容量,那就不需要传,用默认的即可
我们用之前的写法将他们写在不同的文件里,然后进行编译
发现错误,缺省参数不能在声明和定义同时存在,这是害怕给的缺省参数不一致
正确的写法是声明给,定义不给,即在.h文件给缺省,.cpp里不给
5.函数重载
只看main函数里,大家可能认为我们调用的是同一个函数,在C语言里,这是不允许的,但C++里是可以的
判断函数是否重载,要严格的根据定义来判断,很多人以为和返回值类型也有关,其实是无关的
比如函数名相同,参数相同,返回值不同,也不构成重载
要注意参数的类型顺序不同也构成重载,而不是形参名不同
我们看一个特殊例子
这两个f构成重载吗?
我们严格按照定义来看,答案是构成的,编译也是可以通过的,但是问题是无参调用时存在歧义
接下来我们来看一个问题,为什么C语言不支持重载,C++支持?C++是怎么支持的?
这里涉及到编译链接过程和函数名修饰规则,下面我们来详细讲解
假如,我们上面栈的例子,并加入这两个函数
我们有3个文件,一个Stack.h,一个Stack.cpp,一个test.cpp
C++的编译器和C的编译器走的是基本一致,但有些小细节的不同
编译链接的第一步是预处理,预处理要头文件展开
(不清楚编译链接等等作用的同学可以看我往期的内容)
(16条消息) 程序环境和预处理_KLZUQ的博客-CSDN博客
还要进行宏替换,条件编译,去掉注释等等
预处理完后.h都展开了,就没有.h文件了,此时生成了Stack.i和test.i的文件
接下来是编译,编译时要检查语法,生成汇编代码,生成Stack.s和test.s
接下来是汇编,cpu是看不懂汇编指令的,汇编是符合指令,汇编将汇编代码转换为二进机器制码
生成Stack.o和tets.o文件
接下来是链接,生成可执行程序,在Windows下是.exe文件,在Linux下为a.out或其他名字
在此之前,上面的.i,.s,.o都是独立的,只有链接时才会合在一起(注意,不是合并,是链接)
我们转到反汇编,这里的call是调用其他函数,调用其他函数都会被转换为call加一个地址
函数又是一串指令,调用函数的本质又会跳转到 jmp 指令
最终是为了执行这个函数,要建立栈帧
在我们的编译阶段,这里是拿不到这个地址的(tets.i->test.s)
我们包含.h文件,但.h里是声明,地址在Stack.i里
我们举个例子,我们要去买房子需要50w元,我们差10w元,这时我们想起了下铺的好兄弟,我们告诉他,好兄弟告诉我们没问题,这10w到时候他会帮助你
声明相当于一个承诺,即好兄弟答应你,但此时你并没有拿到钱,有了承诺后,我们就敢去买房子了,我们的编译器也是这样的
这也是为什么这里可以通过,为什么缺省参数要在声明时给定 ,声明的时候就可以拿到,进行检查,但是我们此时并没有拿到地址,也就是我们没有拿到那10w块钱,不过我们是敢去买房子的,交个定金
此时我们回过头来看为什么两个.o文件是链接而不是合并,链接的意思是兑现承诺,找到定义
我们的.o都会生成符号表
比如Stack.o的符号表里就有StackInit:0x112233(地址),StackPush:0x112244,StackPush:0x112255
链接错误就是兑现承诺失败,正常情况下都能兑现成功
补充:我们将两个Push函数屏蔽
这是链接错误
但如果我们是少了一个分号,这是编译错误,编译错误走不到链接那一步
回过头来看我们的问题,为什么C语言不支持重载?因为C语言太直接了,直接使用了那个名字
在符号表里叫StackPush,这里的call也就直接用StackPush
那C++是怎么支持的呢?我们这里用Linux简单演示一下(后续我会出Linux的教材)
这段代码我们完全按C语言去写
我们编译后会报错,我们将代码写为正常代码,即屏蔽一个func函数
然后用gcc -o tetsc test.c
默认不指定会生成a.out,但此时我们指定生成testc
接下来我们用objdump -S testc 这样一句指令
我们就可以看到这些东西,我们来看,C语言是直接用的函数名,所以两个名字相同就冲突了
如果我们使用的是C++的话,我们将屏蔽的代码放出来,我们甚至可以把之前的Stack代码拿过来
使用g++ -o testcpp test.cpp指定生成testcpp
我们继续objdump -S testcpp
此时我们可以看到两个func的名字变了,C语言是直接是func,这里是函数名修饰规则
_Z是编译器规定的前缀,4是函数的长度,比如func是4个字母,后面一个是i,一个是id
是形参类型的缩写,即int,double
这是栈的两个Push,只要参数不同,就会修饰的不同
如果我们自己开发一门语言,修改函数名修饰规则,是否可以让返回值类型与重载也相关呢?
答案是不可以
返回值在调用的时候不会体现,所以编译时就报错了,存在调用歧义,走不到链接那一步
除非把语法也改了,我们上面代码返回值不写时知道默认调用哪个
补充:并不是所有的函数都要链接,我们直接有定义时就不需要,直接就有地址,举个例子就是我们借10w块钱后,好兄弟挂了电话后就把钱直接转给我们
我们把StackPush的定义屏蔽后编译
会出现这也的报错, ?StackPush@@YAXPAUStack@@H@Z,这些我们正常情况是很难看懂的,除非有文档对照,而g++的就很明了了,大家感兴趣可以去稍微了解一下
由于 C 和 C++ 编译器对函数名字修饰规则的不同,在有些场景下可能就会出问题,比如:1. C++ 中调用 C 语言实现的静态库或者动态库,反之亦然2. 多人协同开发时,有些人擅长用 C 语言,有些人擅长用 C++在这种混合模式下开发,由于 C 和 C++ 编译器对函数名字修饰规则不同,可能就会导致链接失败,在该种场景 下,就需要使用extern "C" 。 在函数前加 extern "C" ,意思是告诉编译器,将该函数按照 C 语言规则来编译。
6.引用
我们在使用C语言时,经常会使用到指针,但是因为指针有时候非常难,所以C++引入了引用
这里的b就是a的别名,&这个符号在C里是取地址(或者与),C++没有引入新的符号,而直接使用了之前的符号,使用引用如上图所示,a和b是指向同一块空间的
另外,引用是不能这样写的
引用是别名,但不能不告诉是谁的别名,指针是可以不指向的
这个操作并不是将b改为c的别名,而是将c的值赋值给b,此时a也变成200
引用是不能改变指向的(java等语言是可以改变指向的)
我们知道,这样写交换两个数是不能进行交换的,因为形参是实参的拷贝
但是我们改成使用引用,就可以实现了,此时的形参是实参的别名,另外,引用是可以使用缺省的,我们后面会详细讲解
指针的交换也是可以的
我们之前在完成链表时,使用的是二级指针,此时再回来看,我们就可以使用引用完成了,这也是很多书上写的,这些书都夹带了一些私货,这些代码都是需要使用C++才能跑起来的
有些书甚至是这样写的
很多基础不好的同学就直接看懵了,为什么学链表连指针都没有?就是这个原因
我们总结一下引用的特性
1. 引用在 定义时必须初始化2. 一个变量可以有多个引用3. 引用一旦引用一个实体,再不能引用其他实体
下面我们来看看引用的使用场景
一个是做参数(输出型参数)
输入型参数是传给你用的,输出型参数是改变后也会影响外面
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
另外,C++是可以这样写结构体的
在C语言里,我们的next指针前还必须有struct,但是在C++里就不需要了,这是因为C++把结构体升级成了类,可以直接使用类名
引用做参数的第二层意义还有提高效率(大对象、深拷贝)
大对象顾名思义,就是sizeof时比较大的对象,比如
这个对象有4w字节
#include<iostream>
using namespace std;
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main() {
TestRefAndValue();
}
这段代码可以对比效率,我们来看运行结果
引用不会开空间,形参就是实参的别名
当对象更大时,效率差距也更大
不过引用可以做到的事情,指针也可以做到,只是指针稍微麻烦点而已,并没有质的提升,另外,引用并不能完全替代指针,我们这里不做讨论
引用还能做返回值
我们先看这段代码
这里的n并不能直接返回,会先生成一个临时变量,有可能会用寄存器代替(不一定是寄存器,数据量小的时候有可能),再将临时变量给ret,那这里为什么会生成一个临时变量呢?因为出了栈帧就销毁了,但是我们上面的代码n是加了static的,n是在静态区的,不会销毁,但返回n时任然会生成临时变量,这里不会做特殊处理,如果我们不想生成临时变量怎么办?所以就有了下面的内容
引用做返回值
这里返回的是n的别名,n的别名,如果这里的n是大对象,或者是深拷贝时,就有很大的效率提升
#include<iostream>
using namespace std;
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main() {
TestReturnByRefOrValue();
}
这是效率对比代码,我们来看运行结果
效果还是非常明显的
那我们后续都可以用引用返回吗?我们来看下面的一些例子
这段代码其实就有很大的问题,如果我们在这里打印ret,结果是不确定的,这里是一个野指针的问题
我们用引用返回的是n的别名,但是这里n所在的空间已经销毁了,在这个空间里的数据是没有保证的,如果count函数结束,没有清理栈帧,那么ret的结果侥幸是正确的,如果清理了栈帧,ret的结果就是随机值
我们再看这段代码,这个写法的意思是,ret是个引用,返回的是n也是引用,就是我们上面说的多次取别名,ret就是n的别名
我们先打印他们的地址,是一样的,说明是同一块空间
为了更好的演示,我们给count函数加个参数,我们会看到这样的结果
这里的11和21也有可能会是随机值,原理之前相同
我们第二次不调用count,而是随便调用一个别的函数,这里都变成随机了,这就是因为后面函数的栈帧覆盖原来的栈帧,在同一个空间的位置,ret被覆盖了(不知道函数栈帧的同学建议去学习一下函数栈帧,后续会经常提到这个概念)
用引用做返回值是很危险的,但是上面我们用static修饰n时是不危险的,栈帧销毁后n仍然存在
简单总结,出了作用域,在栈帧里面就很危险,不在栈帧里就不危险,传值返回有两次拷贝,先拷贝给临时变量,再将临时变量拷贝到接收值里,传引用返回没有拷贝
基本任何场景都可以用引用传参,但是要谨慎用引用做返回值,出了函数作用域,对象不在了,就不能用引用返回,对象还在就可以用引用返回
可以用引用返回的我们来举些例子,比如static,全局的,malloc的等等
我们来看一个例子
我们在C语言阶段,要完成这些内容,需要按照上面的方式这样做,有人可能觉得没什么,那我们再看看C++如何完成
是不是比起C语言来舒服了很多?我们用一个函数就可以完成这么多的功能
这也就是引用做返回值的第二个能力,修改返回值+获取返回值
我们前面说过,在C++里结构体被升级成了类,所以在C++里,我们真的要实现上面的功能,还能更简单一点
这些我们后面都会详细讲解
我们再看一个常引用的问题
这里的b并不能成为a的引用,a自己都不能改变,b就更不可能变成a的别名改变a了
引用的过程中,权限是不能放大的
这个是正确的,这里是一个拷贝,d的改变不会影响c
这里是可以的,引用的过程中,权限可以平移或者缩小,x自己本身可读可写,变成z是只读,这就是权限的缩小(此处缩小的是z的权限)
++x是可以的,但是++z是不行的
这条语句是可以的,是权限的平移,都是不能修改的
我们可以将a拷贝给b,这是类型转化问题,但是c不能做a的引用
但是我们加上const就可以了
在a拷贝给b时,也创建临时变量(发生类型转化都有临时变量),会先把a给临时变量,再把临时变量给b,下面a给c时,也是有临时变量的,临时变量为什么不可以给引用呢?因为临时变量具有常性,所以加上const就可以了
我们接着往下看
为什么这里的ret1可以接收,而ret2不可以接收呢?
因为这里返回的也是临时变量,临时变量具有常性,这里发生了权限放大
我们加上const就可以了
这三个都是正确的,ret2是权限的平移,ret3是权限的缩小(记住引用做返回值不会产生临时变量) ,另外记住,相同类型不会产生临时变量,类型转换才会(即不同类型)
if语句里,>两边的类型不同,使 i 发生类型提升,提升并不是将 i 本身提升,而是产生临时变量,
这个临时变量是double的,再用临时变量和 j 去比较
我们再看下一个问题
引用在语法层面上不开空间,ra是对a取别名,而pa是指针,需要开空间,存储a的地址
我们对ra改变和对*pa的改变,a都会跟着改变
我们从底层来看,lea是取地址的意思,放到exa寄存器里
我们发现底层汇编指令实现的角度来看,引用是类似指针的方法实现的,也就是底层没有引用,只有指针
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。2. 引用 在定义时 必须初始化 ,指针没有要求3. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体4. 没有 NULL 引用 ,但有 NULL 指针5. 在 sizeof 中含义不同 : 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4个字节 )6. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小7. 有多级指针,但是没有多级引用8. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理9. 引用比指针使用起来相对更安全
7.内联函数
我们在调用函数时,是有很多消耗的,比如建立栈帧等等
假如我们这里有一个函数,我们要频繁的调用它,就会频繁的建立栈帧
我们在C语言时,可以用宏来解决,大家还记得如何写宏吗?
如果大家忘记的话,可以看看我往期的博客
(23条消息) 程序环境和预处理_KLZUQ的博客-CSDN博客
宏的优势是不需要建立栈帧,提高调用效率,有可维护性的优点
缺点是复杂,容易出错,比如这里可能就有很多人忘记怎么写的,可读性差,不能调试等等
所以在C++里给出了内联函数来解决这个问题,内联函数非常简单,只需在我们的正常函数前加一个关键字就行
这个关键字就是inline,内联函数不需要建立栈帧,可读性好,不复杂,可以调试,那我们是否可以将所有的函数改成内联函数呢?
答案是不行的,内联和宏一样,是会展开的,适用场景都是短小的,频繁调用的
我们假设有一个函数func,编译好后的指令有50行,我们有一个项目,有一万个位置调用这个函数,如果func不是内联,合计10000+50条指令(我们之前看到函数调用都是call指令),但如果是内联,那就是10000*50条指令了,是非常恐怖的,会使可执行程序变大,如果我们是开发软件的话,会让安装包变大
由于害怕有人滥用inline,其实inline对于编译器仅仅只是一个建议,最终是否成为inline由编译器决定
一些比较长的函数都不会成为内联(一般编译器设置为5行或者10行),递归也不会成为内联
我们来看看成为内联是什么状态
我们发现,这里还是call,这是为什么呢?
因为默认debug版本下,内联不会起作用,否则无法调试了,所以这里需要我们设置一下
设置完后重新编译就行了
此时我们再看就没有call add了
下面的call调用的是别的东西,不是add,我们之后会讲
我们可以加长代码,干扰编译器,让add不再是内联
我们接着往下看
此时我们编译就会报错,无法解析的外部符号,我们知道这是链接错误,这是什么情况?
有定义为什么找不到,这是因为main函数里编译后发现func是内联函数,是内联函数的话就需要展开,但是想展开却只有声明,那就只能call地址了,但是,内联函数是不会进符号表的,不会生成地址,因为内联函数是直接展开,所以不会进符号表
所以,内联函数的声明和定义不能分离
8.auto关键字(C++11)
auto可以根据右边的表达式来自动推导类型,比如上面的c和d的类型
我们可以使用typeid来打印类型
我们来看个例子
这是以后我们要学习这样的代码,it前面的内容是不是很长?我们此时就可以使用auto
是不是就非常舒服了?
甚至还有这样的代码
这就是auto的作用
不过,auto不能作为函数的参数
auto不能直接用来声明数组
auto可以根据右边表达式自动推导类型,但是上面的b,就指定必须是指针类型,c就指定了是引用
比如这里的b,如果不是指针就会报错
9. 基于范围的for循环(C++11)
这是我们C语言时访问数组的方式
这是C++访问数组的方式
这种方式叫做范围for,他会依次取数组中的数据赋值给e,自动迭代,自动判断结束
那我们可以用它修改数组数据吗?
答案是不可以,因为这是将数组里的值赋值给e,e的改变并不会影响数组
但是我们用引用就可以解决这个问题,此时的e就依次是数组里元素的别名,所以就可以修改了
而且名字不一定叫e,大家取喜欢的就行,并且前面的类型不用auto也可以,我们的数组本身就是int,用int也可以,但是我们推荐使用auto,因为数组变时,auto也会自己跟着变,而不用我们去下面改代码
这样是错误的,因为参数array[ ]的本质还是指针,我们是不知道数组范围的
10.指针空值nullptr(C++11)
我们先看一个问题
我们看到,NULL和0都调用的是第一个f,而强转为int*后的NULL才会调用第二个f
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
所以在C++11为了解决这个问题,引入了nullptr
1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的 。2. 在 C++11 中, sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr 。
以上就是本期的全部内容,希望大家可以有所收获
如有错误,还请指正