C++基础知识-2

本期我们接着来讲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.函数重载

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如中国的兵乓球和男足,两个都是谁也赢不了,虽然是一句话,但意思却完全不同
函数重载: 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这些同名函数 的形参列表 ( 参数个数 或 类型 或 类型顺序 ) 不同 ,常用来处理实现功能类似数据类型不同的问题。

 只看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块钱后,好兄弟挂了电话后就把钱直接转给我们

Windows 下名字修饰规则

 我们把StackPush的定义屏蔽后编译

会出现这也的报错, ?StackPush@@YAXPAUStack@@H@Z,这些我们正常情况是很难看懂的,除非有文档对照,而g++的就很明了了,大家感兴趣可以去稍微了解一下

extern “C”
由于 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是一颗糖果,我们先来简单看看它的用法

 auto可以根据右边的表达式来自动推导类型,比如上面的c和d的类型

我们可以使用typeid来打印类型

 我们来看个例子

这是以后我们要学习这样的代码,it前面的内容是不是很长?我们此时就可以使用auto

是不是就非常舒服了? 

甚至还有这样的代码

 这就是auto的作用

不过,auto不能作为函数的参数

auto不能直接用来声明数组 

为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法
auto 在实际中最常见的优势用法就是 C++11 提供的新式 for 循环,还有 lambda 表达式等进行配合使用。
我们再来看一些东西

auto可以根据右边表达式自动推导类型,但是上面的b,就指定必须是指针类型,c就指定了是引用 

比如这里的b,如果不是指针就会报错 

 9. 基于范围的for循环(C++11)

这是我们C语言时访问数组的方式 

这是C++访问数组的方式

这种方式叫做范围for,他会依次取数组中的数据赋值给e,自动迭代,自动判断结束

那我们可以用它修改数组数据吗?

答案是不可以,因为这是将数组里的值赋值给e,e的改变并不会影响数组

但是我们用引用就可以解决这个问题,此时的e就依次是数组里元素的别名,所以就可以修改了

而且名字不一定叫e,大家取喜欢的就行,并且前面的类型不用auto也可以,我们的数组本身就是int,用int也可以,但是我们推荐使用auto,因为数组变时,auto也会自己跟着变,而不用我们去下面改代码

 这样是错误的,因为参数array[ ]的本质还是指针,我们是不知道数组范围的

for 循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供 begin end 的方法,begin end 就是 for 循环迭代的范围。

10.指针空值nullptr(C++11)

我们先看一个问题

 我们看到,NULL和0都调用的是第一个f,而强转为int*后的NULL才会调用第二个f

NULL 实际是一个宏,在传统的 C 头文件 (stddef.h) 中,可以看到如下代码
#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

以上就是本期的全部内容,希望大家可以有所收获

如有错误,还请指正 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值