系列文章目录
精学C++系列
1 精学C++第一站
感谢佬们支持!
目录
系列文章目录
- 前言
- 一、C++关键字
- 二、命名空间
- 命名空间的嵌套
- 命名空间的名字
- 命名空间的使用
- 三、C++输入输出
- 四、缺省参数
- 概念
- 缺省参数的分类
- 1 全缺省(什么都不传)
- 2 半缺省(传几个且不全传)
- 3 全传
- 五、函数重载
- 定义
- 规则
- 原理
- 六、引用
- 引用特性
- 引用的应用
- 做参数
- 做输出型参数
- 做返回值
- 小总结
- 常引用
- const引用/右值引用
- 指针和引用的区别
- 底层
- 七、内联函数
- 延伸
- 八、auto关键字、范围for
- 例子
- 范围for
- auto使用细则
- auto与指针引用结合起来使用
- 在同一行定义多个变量
- auto不能推导的类型
- auto不能作为函数的参数
- 也不能用来声明数组
- 九、nullptr
- 总结
前言
(C++的博客也是拖着一直没写)
C++的学习是非常漫长且艰辛的,作为最早的面向对象的语言,会有一些语法设计上的坑;而其他语言由于发展较晚,在设计语法时便可以规避这些坑.就我而言,C++的学习相比C,数据结构需要另一种思路:简单的语法学习后,更多的要学习思想:面向对象的封装继承多态;还有对STL的学习,要模拟实现,学习接口,还有模板等等.
多的不说,我们直接开始!
一、C++关键字
早期的C++在关键字上比较C语言没有增加太多,毕竟最开始只是为了在兼容C语言的基础上来解决一下遗漏的一些问题。
C++98/C++03 关键字(共63个)
从C语言继承的关键字(32个):
auto(C++98中含义不同,C++11后重新定义)breakcasecharconstcontinuedefaultdodoubleelseenumexternfloatforgotoifintlongregisterreturnshortsignedsizeofstaticstructswitchtypedefunionunsignedvoidvolatilewhile
C++新增的关键字(31个):
asm(汇编语句)boolcatchclassconst_castdeletedynamic_castexplicitfalsefriendinlinemutablenamespacenewoperatorprivateprotectedpublicreinterpret_caststatic_casttemplatethisthrowtruetrytypeidtypenameusingvirtualwchar_texport(C++98/03有,但很少用)
C++11 新增关键字(10个)
alignas(对齐说明符)alignof(对齐查询)char16_t(16位字符类型)char32_t(32位字符类型)constexpr(常量表达式)decltype(类型推导)noexcept(异常说明符)nullptr(空指针常量)static_assert(静态断言)thread_local(线程局部存储)
注意:
auto在C语言和C++98中是存储类说明符,表示自动存储期;在C++11中重新定义为类型推导关键字。register在C++11中被弃用,C++17中移除。export在C++98/03中用于模板分离编译,但实现困难,C++11中已弃用。- C++11还引入了
final和override作为上下文关键字(不是保留关键字,但有特殊含义)。
从整体来看,早期的c++在设计关键字还是相对保守的
学完这些关键字我们就会发现,他们还是好理解的
只不过从C++11之后,新功能就变多了,比如constexpr,exchange,quoted等,
你说他没用吧,学了之后发现确实设计出来有用,但是感觉用的不多
如果你想稍微学学,万一用的时候再查,不深入学就有点记不住
不学的话感觉又很落伍 C++都快蜕变成一个新语言了
不过这都是学习到后期才该考虑的东西了
二、命名空间
在学习C++语法之前,我们要对C++的语法有一个总体的认知就是
C++的新语法大多都是解决C语言的遗留问题或者对C语言进行优化的
有了这个认知之后我们的学习思路便会清晰很多
命名空间解决的是命名冲突的问题。
例
int rand=0; int main() { printf("%d",rand); return 0; }结果为0
倘若我们引出头文件<stdlib.h>的话
就会是这样
redefinition重定义 所以是因为变量名rand和库函数rand冲突了!所以报错
所以对于命名冲突问题,我们会遇到这么几种情况
1 我们定义的函数,可能会和库里的有冲突
2 进入公司做项目后/或者和别的同学协同开发时,由于项目可能会很庞大,每个人和每个人对函数、变量等命名可能会相同。
C语言无法解决,除非是换名字,这样的话名字就会变得冗长,还不易协作
于是引出命名空间namespace,把变量包进去
#include<stdlib.h> #include<cstdio> namespace Sun { int rand=0; } int main() { printf("%d",rand); return 0; }但是打印出来却不是0
我们补充一个问题
在C/C++中,两个大括号框起来的区域就是一个域。
默认的是全局域,函数内部为函数域,命名空间内部为命名空间域。
而命名空间域本质其实是全局域,所以rand本质也是一个全局变量,会放到静态区
对于域来说,要么影响生命周期,要么则影响访问
对于搜索变量定义来说优先级是这样的
首先找局部域,再找全局域,如果不指定命名空间域或者展开命名空间,他就不主动进去寻找
所以弄来弄去,最后这个rand是在main函数的局部域找的,由于包了stdlib的头文件
所以rand对应的是那个随机数函数,打印是函数名,也就是函数的地址咯
要想达到目的,所以只能这样
#include<stdlib.h> #include<cstdio> namespace Sun { int rand=0; } int main() { printf("%d",Sun::rand); return 0; }指定了命名空间域就好啦~
就可以打印出0
命名空间亦可以定义函数,类型。因为这些都有可能冲突
#include<iostream> using namespace std; namespace SunS { int rand=10; int Add(int a,int b) { return a+b; } struct Node { int val; struct Node* next; }; } int main() { cout<<SunS::rand<<endl; cout<<SunS::Add(1,2); SunS::Node head; head.val=0; head.next=NULL; }命名空间还可以嵌套!
namespace N1 { int a; int b; int Add(int left,int right) { return left+right; } namespace N2 { int c; int d; int Sub(int left,int right) { return left-right; } } } int main() { cout<<"加法 "<<N1::Add(1,2)<<endl; cout<<"减法 "<<N1::N2::Sub(3,1)<<endl; }图示代码在N1命名空间中又嵌套了另一空间N2
所以调用的时候也要逐层的调用
命名空间的名字
命名空间的名字是可以相同的,如果名字相同的话不同地方的两个命名空间会合并到一个命名空间。
命名空间的使用
1 加命名空间名称及作用域限定符(能做到最好的命名隔离,但是它使用不方便)
例:
#include<iostream> int main() { std::cout<<"xxx"; }每次使用命名空间域里的内容,都要加上限定符,非常麻烦但是非常安全。
2 全部展开(最无脑的做法 用起来方便但有风险)
全部展开 即展开全局了 隔离实效了。所以这个行为是慎用的,但平时是无所谓的。
例:
#include<iostream> using namespace std; int main() { cout<<"xxx"; }3 单独展开某一个,其他的不展开(用于展开命名空间中常用)
using std::cout;相比前两种,这种做法又没有风险又方便。
一般我们只展开最常用的展开
三、C++的输入输出
首先我们得先来介绍一下C++的新的头文件规则
我们知道c语言的风格是以.h结尾的,当开始C++后,祖师爷为了解决臃肿的头文件格式,就改成了不带.h的简洁版,而对于旧的c语言头文件,也给了一种新的格式,即c开头的。例如<cmath>
最清晰的格式见如下图:
(摘自第六版C++ primer plus)
#include<iostream> using namespace std; int main() { cout<<"xxx"; }但是为什么包了头文件,还必须展开命名空间呢?
因为c++库的实现放在了std命名空间里,这个库也是c++标准库。
下来我们开始正式介绍C++的输入输出
简单来说
cout == printf
cin == scanf
endl == \n
类比下来是这样
cout<<"xxx"<<endl; printf("xxx\n");正式介绍<<
流插入操作符(看着很像移位操作符?)
当然,相对于printf,cout和<<还是有优势的!就是对于不同类型,不用写对应的格式
也就是自动识别类型,他的底层我们暂且不论。
也就是如下操作
int i=10; double d=3.14; cout<<i<<" "<<d<<endl; printf("%d %lf",i,d);不过显然,如果想控制小数位数这类“定制输出”的话,C语言会比C++方便很多
对应我们有>>
cin>>i>>d;当然,cin也可以自动识别类型。
四、缺省参数
概念
所谓缺省参数,是声明/定义函数时为函数的参数指定的一个缺省值,就是默认值
如果我们传入了值,就用这个值;否则就用缺省的默认值
例:
void func(int a) { cout<<a<<endl; } int main() { func(1); }如果我们不想传参数 就在形参后面给一个缺省
void func(int a=0) { cout<<a<<endl; } int main() { func(); }缺省参数的分类
例:
#include<iostream> #include<cstdio> using std::cout; using std::endl; void func(int a=10,int b=20,int c=30) { cout<<a<<" "<<b<<" "<<c<<endl; } int main() { return 0; }1 全缺省(什么都不传!)
所谓全缺省,就是所有参数都用给定的缺省值,一个都不传
func();2 半缺省(传几个且不全传)
半缺省的话就是传入部分参数,其余的使用缺省参数。
不过要注意,这个传参顺序是固定的。不能指定只传某一个。
毕竟解析参数是从右向左,所以如果要使用缺省值的话必须从最右边的参数开始,中间不能间隔。
func(1);//a=1 func(1,2);//a=1,b=23 全传
func(1,2,3);//a=1,b=2,c=3另外,缺省参数必须是常量/全局变量,且带有缺省参数的函数不能在定义和声明中同时出现
(因为怕两个地方不一样)
例如
test.h中
void func(int a=10);test.cpp
void func(int a=20) {}五、函数重载
函数重载,这里要理解为一个函数有多种功能。
但是按照底层原理来看就是多个函数分别实现了不同功能,但是他们有相同的函数名
定义
具体定义是这样的
C++限定了在同一作用域中,可以有几个功能相似的同名函数
这些函数只要在以下三个里面有一个满足即构成函数重载
规则
1 类型不同
void add(int a,int b) { cout<<a+b<<endl; } void add(double a,double b) { cout<<a+b<<endl; } int main() { add(1,2); add(1.1,2.2); return 0; }这里同时控制参数的类型不同,别的都相同
2 参数个数不同
int A(int a) { return a; } int A() { return 1; } int main() { cout<<A()<<endl; cout<<A(2)<<endl; }3 参数的顺序不同
void f(int a,char b) { cout<<"int a,char b"<<endl; } void f(char b,int a) { cout<<"char b,int a"<<endl; } int main() { f(10,'c'); f('c',10); return 0; }相同的参数,改变他的顺序,也可以构成重载。
(注意!返回值不同不能构成重载?)
因为我们调用的时候如果不接受返回值,那就无法区分两个函数了
例:
给定
int f(double j) void f(double j) f(1.1) f(1.1)再来一个容易混淆的点。如果是以下这种情况,还能构成重载吗
都是带有缺省参数的情况
1 一个是参数a,一个是参数a并带有缺省参数
void f(int a) { cout<<"f(int b)"<<endl; } void f(int a=10) { cout<<"f(int a)=10"<<endl; } int main() { f(10); f(); }两次调用都不能成功
如果改成这样
一个不带参数 另一个是有参数并加上缺省参数
void f() { cout<<"f(int b)"<<endl; } void f(int a=10) { cout<<"f(int a)=10"<<endl; }如果传参
f(5);不会报错,毕竟这里指定了需要参数,所以只能匹配第二个f
如果不传参数的话
这样两个函数都可以被匹配,还是不行的
f();原理
那么为什么可以这么做呢?原理是什么?
首先C一定不支持,而C++却支持
简单来看,我们在调用一个函数时,一定会用到汇编的一个指令,那就是call或者bl
例如如下代码
int add(int a, int b) { return a + b; } int main() { add(1,2); }bl __Z3addiibl是调用,后面那一坨是我们的函数名
他的规则是这样的
__Z:这是名称修饰的前缀,表示这是一个 C++ 函数(非 C 语言函数)所有 Itanium ABI 下 C++ 函数名的固定前缀。3add:3表示函数名add的长度为 3 个字符,add是原始函数名。ii:表示函数的参数类型。每个i代表一个int类型参数,所以ii表示该函数接受两个int类型参数。
所以只要我们控制参数的个数、类型、顺序 就能生成不同的,被修饰后的函数名 调用起来自然就没有歧义啦
bench 在这里可以理解为对这段汇编调用进行基准测试(benchmark)的简写,用来验证不同重载函数在调用时的性能差异。
六、引用
引用算是又一个C++引入的新东西,由于指针的管理较为困难,所以引入了引用。
引用的核心就两个字:取别名。他不是定义一个新的变量,而是对旧的变量去啦一个别名
编译器不会为引用变量开辟空间。他和他引用的变量占一块空间。
例:
#include<cstdio> int main() { int a=10; int &b=a; int *p=&b; printf("a:%p,b: %p,p: %p",&a,&b,p); }其中,b为引用变量,p为指针
1 引用特性
首先 引用在定义时必须初始化
例:
int a=10; int &b;其次 一个变量可以有多个引用 (也就是一个变量可以有多个别名)
int a=10; //a为变量 int &b=a; //b为引用 int &c=b; //c为引用当然 由于引用本质也是个变量 我们也可以给引用变量再起别名
int &d=b;一旦引用了某个实体 就不能再引用其他实体了
int a=10; //a为变量 int &b=a; //b为引用 int c=20; //c为变量 b=c;对于这一段代码的最后一行 C++和java的理解是不一样的
由于C++的引用不能修改指向
所以b=c;的意思是把c的值赋值给b 也就是仍在基础变量的范畴 不涉及引用
修改了b之后,由于b为a的别名 所以a也为20
只不过由于是赋值 所以c的地址与a,b不同
如图所示
2引用的应用
引用出现之后,还是可以给我们的编码方案提供一种新的选择的
做参数
举一个简单的例子 我们写一个交换两个变量值的函数swap
之前我们写的话 要通过指针
传参的时候也得传地址
void Swap(int *a,int *b) { int temp=*a; *a=*b; *b=temp; } int main() { int a=0; int b=1; Swap(&a,&b); cout<<a<<" "<<b<<endl; }现在有了引用之后,我没可以直接用引用作为参数
传参是也不用传地址 a1,b1直接作为a,b的别名 修改了a1,b1 主函数的a,b也能被修改
void Swap(int &a1,int &b1) { int temp=a; a=b; b=temp; } int main() { int a=0; int b=1; Swap(a,b); cout<<a<<" "<<b<<endl; }同时你会发现 两次的函数都使用了同一个名字Swap,但是调用也不会有报错
因为传引用传参和传指针传参构成了函数重载
但是坏消息是 如果此时增加一个传值传参的版本
虽然也不会报错
但是Swap(a,b)两个函数都可以调用 会有调用歧义的问题
输出型参数
输出型参数是我们很熟悉的问题 最早是由于c语言函数只能返回一个值
所以一旦我们想带出其他的值 需要传入一个指针变量作为输出型参数
例如
力扣有个题是这样的
他的c语言借口模板是这样的
其中 nums是原数组,numsSize ,其中returnSize是我们要返回数组的长度,也就是2
(这个虽然原题没有要求 但是是力扣要求的)
int* singleNumber(int* nums, int numsSize, int* returnSize) { }所以我们在返回数组之前,必须有一步就是 设定returnSize的值
*returnSize=2;当然有了C++之后,我们就可以用引用了
int* singleNumber(int* nums, int numsSize, int& returnSize) { //…… returnSize=2; }做返回值
首先我们回顾传值返回
Add函数返回了c,他返回的是真的c吗?
不是 只是c的拷贝 因为这个c只是临时变量 出了函数作用域就会销毁 到时候c的值就丢了
为了拿到c的值 所以编译器创建了一个c的拷贝
那现在又有问题了 这个c的拷贝放在哪里?
1 如果这个c比较小(4/8个字节) 那就会放到寄存器中
2 如果c比较大 临时变量就会放在调用Add函数的函数栈帧中
此时如果改成传引用返回
由于返回值变成了c的别名 所以不会生成临时变量
但是这其中又有了新的问题
1 首先c依然是临时变量,出了作用域还是会销毁,那我们访问c的时候实际上是在访问已经销毁的空间
2 访问之后 如果原来c的空间里的内容没有被清理 我们还是可以拿到正确的值
但如果被清理了,那我们取到的就是一个随机值
这个“清不清的行为”是取决于编译器实现的
当然如果有更严格的编译器 你根本无法访问已经销毁的空间
就算我们某次侥幸拿到了正确的值,也是很容易制造出问题的
例如
int a,b=1; int& ret=Add(a,b); cout<<ret<<endl; cout<<"11111111111\n"; cout<<ret<<endl;我们通过一个cout,成功把c的空间覆盖了 所以ret也就变成了1
小总结
我们对以上知识做一下总结 再给一个例子 这样不仅可以加深我们的理解 面试的时候给面试官讲故事也比生涩的背书效果更好
我们使用内存就像向房东租房子
当你租完退房的时候,假设你在房子里面留了一件你的物品
如果恰好没人来住(没有人来访问空间),或者看到这个物品没有动 那就没事
但如果有人来占了房子,看见了这个物品,发现不是自己的,占了
放了别的东西,你再来找你的东西
假定新的租客没发现你,你也不能发现你的东西了
那么这段话有什么用呢?
所以我们得出以下规则
如果函数返回时,出了函数作用域之后如果返回对象还在
那么就可以使用传引用返回
如果已经返回给系统了
就必须使用传值返回
目前我们可以先给一个不销毁的例子
int &count() { static int c=0; c++; //…… return c; }再次总结
1 引用在传参数和传返回值在某些场景下,可以提升性能
2 引用在传参和传返回值时做输出型参数和输出型返回值
常引用
我们先给两组代码
int a=10; int &b=a;这样是没问题的
但是下面这样是不行的
const int c=10; int &d=c;(报错)
为什么呢?这就要引出一种叫权限的概念
权限可以缩小,权限可以平移,但是权限不能放大
什么意思呢?我们拿代码来看看
看第一段代码
a是正常变量,能读能写,b也是正常的引用变量,所以用b当a的别名属于是"权限平移"
看第二段代码
c是一个const变量,只能读不能写;但是d是一个正常变量,能读能写,你让一个能读能写的正常变量去当const变量就是想把权限放大,这是不行的
所以我们可以对这段代码做修改,变成权限平移
const int c=10; const int &d=c;我们再给一段权限放大的代码
众所周知,类型转换,截断,整形提升都会生成临时变量,而临时变量一定是一个const变量
double dd=1.11; int ii=dd; //int &rii=dd; //权限放大 const int &rii=dd;第三行代码中,如果想用rii去给dd当别名
首先dd会先类型转换成int变量(正如第二行所示),生成一个类似ii的临时变量
然后再让rii去充当dd的别名
所以看似是rii充当dd的别名,其实是给ii这个临时变量充当别名
所以会有权限放大的问题,一定会报错
而改成const,则没有问题
我们再给看看权限缩小
所谓权限缩小,就是让一个又能读又能写的变量变成只能读的变量
int i3=0; const int &ri3=i3;或者说我们给个拷贝
const int c3=0; int d3=c3;由于拷贝不会修改原有的变量,所以不涉及权限.所以无论谁的拷贝赋值给谁都可以
只有引用和指针才涉及到权限的问题
我们再系统地给一组例子
第一个函数我们返回值类型为int,所以返回的一定是x的拷贝,也就是一个临时变量
int func1() { static int x=1; return x; }这个时候接受返回值就会涉及到权限问题了
int &ret1=func1(); //权限放大 const int&ret2=func1(); //权限平移 int ret3=func1(); //只是拷贝,不涉及权限那我买再来修改一下代码
上面我们刚刚学习到,如果一个函数要返回的值在函数销毁以后不销毁,那么就可以使用返回引用
int& func2() { static int x=2; return x; }下来我的权限问题就会变成这样
int &ret1=func2(); //权限平移 const int&ret2=func2(); //权限缩小所以如果x作为一个大对象或者后面学习的深拷贝的对象,那么我们是应该用引用返回的,达到减少拷贝的目的.而如果其中不涉及对x的修改,我们可以改成const 引用返回
当然传参也是一样的
const引用/右值引用(借用c++ primer plus的代码)
我们延伸一下对const引用传参的讨论,借用一下C++ primer plus的代码
如果我们直接使用引用传参,这个参数就会有被修改的风险
double cube(double a) { a*=a*a; return a; } double refcube(double &ra) { ra*=ra*ra; return ra; } int main() { double x=3.0; cout<<cube(x)<<"is cube of"<<x<<endl; cout<<refcube(x)<<"is cube of"<<x<<endl; }这一段代码非常的精妙
首先第一行不用多说,只是一个拷贝
下来第二行,在调用refcube之后,x的值在ra被修改的时候也被修改了,所以最后都是27
但是传引用传参时,必须保证实参数是正常变量,而不是一个具体的值或者别的
例如如下
cout<<refcube(x+2.0)<<"is cube of"<<x<<endl;x+2.0是一个表达式,他的结果是一个临时变量,一定不是一个变量,所以不能用引用接受传参
那这样怎么办呢
既然你的表达式结果是一个常量,我们不妨使用const引用 (当然这样的话,ra就不能修改了)
double refcube(const double &ra) { //ra*=ra*ra; return ra*ra*ra; }也就可以顺利接受参数为x+2.0的传参啦
当然不论我们传什么样的参数,ra始终是一个const别名,不会做修改
还能怎么办呢
于是我们引出了左值右值的概念
我们把可以取地址的称为左值,或者表现为可以出现在赋值符号的左边和右边
我们把不能取地址的称为右值,可以出现在赋值符号的右侧,但不能是左侧
左值我们很熟悉,右值一般是什么呢?字面值,表达式返回值,函数返回值(当然得是传值返回)
10; x+y; fmin(x,y);所以要想传参数,我们也可以使用C++新出的右值引用
double Cude(double &&d) { return d*d*d; } int main() { double a=3.0; double z=Cude(a+3.0); }那么这个和上面的const引用有什么区别呢?
相比于const引用既可以接受左值,又可以接受右值;而且还不能区分你传的是左值还是右值
右值引用由于只能接受右值,更像是在倒逼传参的程序员,你必须传一个右值给我,或者move(左值);
涉及到自定义类型尤其还有深拷贝的话,如果程序员不写一个移动构造的话,那么遇到拷贝构造或者赋值的时候只能走拷贝构造而非代价更小的移动构造了
指针和引用的区别
指针和引用用途上是相似的,只有两方面不同
1 首先是使用场景
由于我们的引用在定义时必须初始化,而指针不做要求
所以例如我们熟悉的链表就只能由指针实现
struct ListNode { int data; ListNode *next; };2 其次也就是一些莫名其妙的语法特性
- 指针可以在任何时候指向一个同类型实体,而引用在引用了一个实体后,则不能再引用其他实体(换句话说,指针可以改变指向,而引用不可以)
- 有空指针NULL,但是没有空引用
- 用sizeof计算大小时,sizeof(引用)为引用类型的大小,而sizeof(指针)永远为4/8字节
- 另外,引用+1即引用的实体值+1,而指针+1是地址+1个(指向)类型的大小
- 野指针/野引用: 众所周知,指针指向的空间被释放后会出现“野指针”的问题;而其实,如果我们假定引用变量指向的“本体”变量销毁之后,这个引用变量也就变成了类似的“野引用”
底层?
语法层面来看,引用是没有开空间的.而指针开了4/8个字节的空间
但是抛开别的不谈,你不开空间,我们存在哪呢?
我们同时对指针和引用转汇编之后,发现其实是差不多的
所以我们得出结论
引用的底层还是指针实现的.
即使上层差异很大,但是下层是一样的
只不过在平时的探讨中,探讨引用的底层压根没有意义,我们只需要知道他是给变量的别名,这就够了
七、内联函数
以inline函数修饰的函数叫内联函数,条件符合时,编译时C++会在调用内联函数的地方展开
没有函数压栈的开销,提高程序运行的效率。
内联函数是用来对标宏函数的,是一种相对于宏函数更安全的函数
你 比方说,你有一个短小的函数(1-10行),比方说swap函数
会有频繁的调用(10w次)(调这么多次,开销太大了)
如果是C语言会怎么做呢? ->宏函数
如果是C++呢?C++的大佬觉得宏函数不行,搞出了内联函数进行优化
首先我们先列举宏的几大优点缺点
优点
- 可维护性好 如[(#define N 6)-》 (#define N 7)]
- 效率高 可以减少栈帧建立
缺点
- 可读性差,复杂(要通过加括号来规避一大堆优先级的问题)
- 没有类型安全检查
- 不方便调试
对此我们给出如下例子:
#define ADD(a,b) ((a)+(b))一个看似简单的加法 为什么要加这么多括号呢?
就是为了规避一些场景
场景1:
int a=ADD(1,3)*3;有了最外部的括号,才能得到正确结果12,否则就变成了1+3*3=10了
场景2:
int x=1; int y=2; int a=ADD(x/y,x&y); // <=> ((x/y)+(x&y))有了内部的括号,才能保证x/y和x&y这两个运算可以顺利进行
那既然宏这么恶心,所以内联函数的时候我们就可以写正常的函数了
inline int add(int a,int b) { return a+b; }看起来非常简单 但是我们要注意:
内联函数几乎没有缺点,所以C++里不在使用宏
inline几乎解决了宏函数的缺点,并同时兼顾了他的优点
当然当然,想作为宏函数必须满足条件
那么这个条件是什么呢?
inline关键字对于编译器来说仅仅是个建议,编译器并不放心你是否内联正确,
如果你自己定义的内联函数过长(内部实现代码长度,可理解为汇编),或者有递归等,编译器优化会忽略掉内联
为什么呢?
内联函数是本质一种空间换时间的做法,当有(内联函数过长、递归)两种情况时,同时搭配多次的调用
;此时再展开的话,导致编译出来的程序变大
例:
我没有一个50行的函数,和1000次的调用(每次调用都是一个call func)
| 不内联 | 内联 |
| 1000+50行 | 1000*50行 |
这样就得不偿失了
另外
inline不建议有声明和定义分离,分离会导致链接错误,这是因为inline在编译时展开后,就没有函数地址了,所以也就不会放进符号表里,链接也就无法找到了.
延伸
我这里做一点延伸,用于激发大家的思考
在后面的C++11以及之后的版本中,引入了constexpr这一种关键字,他的作用是对于一些能提交计算、确定结果的“东西”,允许在编译器就确定这个“东西”的值
最开始只能用在一些常量上,在后续的版本才逐步加大范围
除了安全考量之外,constexpr最关键的思想是把一些工作、负担从运行时转到编译时,
inline是在减少运行时的栈帧开销,constexpr是减少运行时的计算,让运行时直接拿到值
这说明C++一直想往同一种设计哲学上靠:
zero-overhead principle"(零开销原则)的一个延伸方向:C++从一开始的设计哲学就是——能在编译期确定的东西,就不要拖到运行时去做,让运行时只承担"真正必须在运行时才能确定"的那部分工作
毕竟编译期跑一遍代码是"免费"的(不占用程序实际运行的时间),运行时的每一条指令才是真正要花用户CPU时间的成本
后续我们学C++的一些看似不相关的特性(模板、constexpr、inline、甚至一部分RAII的设计考量),实际上都在有意无意地往这方面靠,只不过constexpr和inline是把这件事摆在了明面上
八、auto关键字、范围for
例子:
我们先给一个例子
int TestAuto() { return 10; } int main() { int a=10; auto b=a; auto c='a'; auto d=TestAuto(); cout<<typeid(a).name()<<endl; cout<<typeid(b).name()<<endl; cout<<typeid(c).name()<<endl; cout<<typeid(d).name()<<endl; }目前来看,auto的用法还是有点局限的,他要和另一个知识点范围for结合起来一起用才行
范围for
以前我们遍历数组是这样的
int arr[]={1,2,3,4,5,6,7}; for (int i=0;i<sizeof(arr)/sizeof(int);i++) { cout<<arr[i]<<endl; }现在我们可以这样
for (auto e:arr) { cout<<e<<endl; }(这里的e是对其中数组元素的拷贝)
auto使用细则
1 auto与指针、引用结合起来使用
用auto声明指针类型时,用auto和auto*没任何区别
但是用auto声明引用类型时,必须加&
int i=10; auto a=&i; //int * auto*b=&i; //int * auto c=i; //int &这些有什么用呢?
以前我们修改数组元素是这样的
for (int i=0;i<sizeof(arr)/sizeof(int);i++) { arr[i]++; }现在我们修改可以这样
(首先我们知道,直接写e的话是对arr元素的拷贝,所以想要修改必须加上&)
int arr[]={1,2,3,4,5}; for (auto &e:arr) e--; for (auto &e:arr) cout<<e<<" ";但如果用指针的话是不行的,直接就会报错
for (auto *e:arr) e--;因为每次的e获取的是数组的值,也就是int,他无法从int转为int*
2 在同一行定义多个变量
在同一行的多个变量必须是同一类型,否则编译不过
int i=1,j=2; auto a=1,b=2; auto c=3,d=4.0;auto不能推导的类型
1 auto不能作为函数的参数
void TestAuto(auto a) {}2 也不能用来声明数组
auto arr[]={1,2,3,4,5};九、nullptr
首先我们要搞明白为什么要引出这个nullptr
我们在C98中一直用的NULL实际上是个宏 也就是……
#define NULL 0这就导致我们在一些使用场景下可能会出现问题
例:
(但是不知道为什么在clion里面NULL变成long了,g++更严格,直接编译不通过)
void func(long) { cout<<"func(long)"<<endl; } void func(long*) { cout<<"func(long*)"<<endl; } int main() { long*p=NULL; long i=0; func(i); func(p); func(NULL); }所以为了弥补C98的这个坑 C++11引入了nullptr,是作为关键字引入的
引出如下规则:
- 在使用nullptr时,不用引头文件
- 在C++11中,sizeof(nullptr)和sizeof((void*)0)所占字节数相同
- 为了提升代码的健壮性,我们最好用nullptr
总结
做总结,这次是我们学习C++的第一篇博客,由于未进入面向对象,还不算太难,有很多语法细节都是对C做补充和修订.害! 好戏还在后面呢
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。