⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++初阶
⭐代码仓库:C++初阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
C++入门
- 前言
- 一、C++关键字
- 二、命名空间
- 1、开篇解决小问题
- 2、命名空间的定义
- (1)嵌套函数命名空间
- (2)嵌套命名空间
- (3)多个相同名称的命名空间
- 3、命名空间使用
- (1)引例
- (2)三种方法输出命名空间域的值
- i、加命名空间名称及作用域限定符
- ii、使用using将命名空间中成员引入
- iii、使用using namespace 命名空间名称引入
- iv、完整版
- 三、C++输入&输出
- (1)第一个C++程序
- (2)cout和cin
- (3)注意
- i、使用头文件和namespace
- ii、输出输入更加方便
- 四、缺省参数
- 1、形象描述缺省参数(备胎)
- 2、缺省参数概念
- 3、全缺省参数
- 4、半缺省参数
- 5、缺省参数的应用
- 6、声明和定义不能同时给缺省值
- 7、总结
- 五、函数重载
- 1、函数重载概念
- 2、按照参数类型不同
- 3、按照参数个数不同
- 4、按照参数顺序不同
- 5、注意
- 6、C++是为何以及如何实现重载的
- (1)预处理阶段
- (2)编译阶段(单个文件)
- (3)汇编阶段
- (4)链接阶段(兑现承诺)
- i、链接的界面化操作
- ii、链接的含义
- (5)Linux下C++实现重载逻辑(名字修饰)
- (6)特殊情况
- 六、引用
- 1、引用的概念
- 2、引用的地址演示
- 3、引用特性
- 4、常引用
- 5、使用场景
- (1)做参数
- i、做输出型参数
- ii、针对大对象/深度拷贝问题 - 提高效率
- (2)引用做返回值
- i、引例
- ii、正式代码
- (i)错误代码1
- (ii)错误代码2
- (iii)错误代码3
- (iv)正确代码(提高效率)
- iii、效率比较(针对大对象/深度拷贝问题)
- (3)返回值引用可以修改返回值
- (4)总结
- (5)加餐
- 6、引用和指针的区别
- (1)相同点
- (2)不同点
- 七、内联函数
- 1、引例
- 2、概念
- 3、特性
- 八、auto关键字(C++11)
- (1)auto的定义
- (2)auto的使用细则
- i、auto与指针和引用结合起来使用
- ii、在同一行定义多个变量
- iii、 auto不能推导的场景
- (i) auto不能作为函数的参数
- (ii) auto不能直接用来声明数组
- 九、基于范围的for循环(C++11)
- 1、范围for的语法
- 2、范围for的使用条件
- 十、指针空值nullptr(C++11)
- 1、C++98的坑
- 2、nullptr
- 总结
C++之父:本贾尼·斯特劳斯特卢普
前言
C++之父本贾尼是在C的基础之上,容纳进去了面向对象编程思想,并增加了很多有用的库。我们在熟悉了前面所讲解的C语言了以后,对C++学习有一定的帮助。
本文的核心点在于:
1、补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、函数方面、指针方面等。
2、为后续类和对象学习打基础。
一、C++关键字
我们知道,C的关键字有32个,而C++是在C的基础上进行完善的,那么C++的关键字有63个,我们看下表:
二、命名空间
使用命名空间的目的是对标识符的名称进行本地化操作,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
1、开篇解决小问题
我们在学习C++初阶大家肯定会遇见很多人写的==using namespace std;==这个语句,看起来非常的简练,但实际上其中有非常大的奥妙,我们看下面的语句:
我们发现报错了,报的是rand不明确。为什么呢?原因在于std是整个C++库函数的总称,当我们使用namespace展开std库的时候,是把整个C++库给展开了,而rand刚好是std库当中的关键字,如果展开以后再使用那不是重定义了吗?那么这样就不能用,所以我们在未来写工程项目中尽量要避免这样写,因为展开整个库函数,难免会有很多关键字是重复了,会造成重定义。
2、命名空间的定义
用namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。(这个命名空间针对于全局类型变量)
样例:namespace N1 {}
(1)嵌套函数命名空间
利用命名空间,就像是一堵围墙围住了这个所包含的空间,它不像我们前面举的例子是个在全局中展开的std,这个进行花括号所围成的命名空间受到保护,里面的变量命名就不会起冲突,因为这是在不同的函数栈帧上的,相互之间的命名是不会起冲突的。
namespace Rh
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
}
(2)嵌套命名空间
嵌套两个命名空间,相当于故宫中的里三层外三层,继续围墙保护着。
namespace Jr
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
namespace Jh
{
int b = 2;
int Sub(int x, int y)
{
return x - y;
}
}
}
(3)多个相同名称的命名空间
那么我们万一定义了两个一样的命名空间名称,编译器会自动将同名的命名空间合并到同一个命名空间中,所以不冲突,和重命名有本质的区别。
namespace Jr
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
namespace Jh
{
int b = 2;
int Sub(int x, int y)
{
return x - y;
}
}
}
namespace Jr
{
int d = 4;
int Mul(int x, int y)
{
return x * y;
}
}
3、命名空间使用
(1)引例
int a = 0;
namespace Jr
{
int a = 1;
}
//展开了命名空间域 - 相当于暴露在全局,也就是与全局变量同级
using namespace Jr;
int main()
{
int a = 2;
printf("%d", a);
return 0;
}
上面的代码最后输出的是2,因为我们的printf放在的是main函数的围墙保护当中,所以首先输出的是main函数中定义的a的值,所以全局变量和main函数访问不到。
(2)三种方法输出命名空间域的值
i、加命名空间名称及作用域限定符
利用命名空间加上::这个作用域限定符则可以访问到命名空间中的域的值。
int a = 0;
namespace Jr
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
namespace Jh
{
int b = 2;
int Sub(int x, int y)
{
return x - y;
}
}
}
int main()
{
int a = 2;
printf("a = %d\n", Jr::a);
return 0;
}
ii、使用using将命名空间中成员引入
使用using加上命名空间加上作用域限定符加上作用域变量则可以直接访问作用域中的元素的值。
int a = 0;
namespace Jr
{
int a = 1;
int k = 10;
int Add(int x, int y)
{
return x + y;
}
namespace Jh
{
int b = 2;
int Sub(int x, int y)
{
return x - y;
}
}
}
using Jr::k;
int main()
{
int a = 2;
printf("k = %d\n", k);
return 0;
}
iii、使用using namespace 命名空间名称引入
使用using namespace将我们定义的Jr命名空间进行展开,再直接使用即可。
int a = 0;
namespace Jr
{
int a = 1;
int k = 10;
int Add(int x, int y)
{
return x + y;
}
namespace Jh
{
int b = 2;
int Sub(int x, int y)
{
return x - y;
}
}
}
using namespace Jr;
int main()
{
int a = 2;
printf("k = %d\n", k);
printf("a = %d\n", Jr::a);
int l = Add(10, 20);
printf("l = %d\n", l);
return 0;
}
iv、完整版
掌握这个关键的信息:=局部域->全局域->展开了命名空间域 or 指定访问命名空间域。
也就是先访问局部域,再访问全局域,最后访问展开了命名空间域或者是指定访问的命名空间域。
int a = 0;
namespace Jr
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
namespace Jh
{
int b = 2;
int Sub(int x, int y)
{
return x - y;
}
}
}
//展开了命名空间域 - 相当于暴露在全局,也就是与全局变量同级
using namespace Jr;
//局部域->全局域->展开了命名空间域 or 指定访问命名空间域
int main()
{
int a = 2;
printf("a = %d\n", a);
printf("a = %d\n", ::a); //::左边是空白则访问的是全局(::左可不加空格)
printf("a = %d\n", Jr::a);
printf("a = %d b = %d\n", Jr::a, Jr::Jh::b);
return 0;
}
三、C++输入&输出
(1)第一个C++程序
我们在学习C语言的时候,第一个输出的语句为hello word,那我们直接写第一个C++语句跟大家打个招呼吧!
(2)cout和cin
注意:这里的<<和>>符号是是最关键的。
口诀: 1. cout流出所以指向流出,为<<
2、cin流入所以指向需要输入的信息,为>>
3、endl就相当于换行。
图例:
(3)注意
i、使用头文件和namespace
在使用cin和cout的时候需要包含==头文件为include iostream>==以及需要展开以std标准命名空间的库函数。
这里有个小知识:(为何头文件没有加.h)
在早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可(和c一样),后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;而旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用iostream>+std的方式。
ii、输出输入更加方便
我们在输出的时候,是需要加上%d,%c,%f等类型,而使用cin和cout它能自动识别类型并将其匹配,让语句更加地方便,但也同样有一定的风险,导致类型紊乱。
四、缺省参数
1、形象描述缺省参数(备胎)
缺省参数可以理解为备胎,在函数上定义形参,我们如果不进行传参则用的是函数的形参,如果进行传参,则用传参的实参的值,可以形象地把函数中的形参理解为备胎,没有传参的时候用的是形参这个备胎的值,传参就用正式男友这个实参的值。
2、缺省参数概念
缺省参数就是在声明或者定义函数的参数的指定一个默认值。在调用函数的时候,如果没有给实参,则用的是原本声明或者定义函数的参数所指定的那个默认值,也就是这个函数的形参。而如果给了实参,则用的是实参的值。
补充:缺省参数就是形参上是有默认值的,如果形参上是没有值的,那么就不是缺省。
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参的时候,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
3、全缺省参数
顾名思义,全缺省参数也就是函数的形参都是有默认值的:
如上图,该Func函数是一个全缺省函数,因为其形参a,b,c都是有默认值的,所以我们控制main函数中的实参进行传参,注意,这里传参只能从左往右传参,不能间隔着传参,传参了就用传的实参,没有传参就用默认的形参的值(备胎)。
4、半缺省参数
半缺省参数可不是只缺一半的参数,而是缺少部分参数,这里注意的规则是半缺省是从右往左依次来给出的(从右往左依次有备胎),传参是从左往右传参的,不能间隔着给。
5、缺省参数的应用
利用备胎的思路,提前先给一个值用以处理栈空间的问题,我们刚开始想定义一个100或者1000或者10000个空间,但是利用我们之前写的初始化栈的代码似乎不行,因为如果从100改到1000需要改的地方实在是太多了,所以我们利用缺省参数的思路进行操作。
如下操作,我们想插入100个值到数组a里面的话,直接进行数值的更改并插入即可;当我们不确定想要插入多少值的时候,就单纯不传参,用形参中的默认值即可。
typedef struct Stack
{
int* a;
int top;
int capacity;
}Stack;
using namespace std;
void StackInit(Stack* st, int DataCapacity = 4)
{
st->a = (int*)malloc(sizeof(int) * DataCapacity);
if (st->a == NULL)
{
perror("malloc fail\n");
return;
}
st->top = 0;
st->capacity = 0;
}
int main()
{
Stack st;
//插入100个值
StackInit(&st, 100);
//不确定插入多少值
Stack st1;
StackInit(&st);
return 0;
}
6、声明和定义不能同时给缺省值
要证明这个结论成立的话,那我们就用反证法,声明和定义都给缺省值。
Stack.h:
#include<stdio.h>
#include<stdlib.h>
typedef struct Stack
{
int* a;
int top;
int capacity;
}Stack;
void StackInit(Stack* st, int DataCapacity = 4);
Stack.cpp:
void StackInit(Stack* st, int DataCapacity = 4)
{
st->a = (int*)malloc(sizeof(int) * DataCapacity);
if (st->a == NULL)
{
perror("malloc fail\n");
return;
}
st->top = 0;
st->capacity = 0;
}
Test.cpp:
#include"Stack.h"
using namespace std;
void StackInit(Stack* st, int DataCapacity = 4);
int main()
{
Stack st;
//插入100个值
StackInit(&st, 100);
//不确定插入多少值
Stack st1;
StackInit(&st);
return 0;
}
我们直接看报错信息,发现是重定义默认参数,说明声明和定义不能同时给缺省值,因为声明和定义给不同的默认形式参数值到底该听谁的,那不是乱了套了吗!?
问题来了:到底是声明给还是定义给?
我们试一下先只给定义看报不报错,再只给声明看报不报错:
1、只给定义:
发现是错误的。
2、只给声明:
是成功的。
原因:我们可以这样理解:定义并不关心传参有多少,本来就有这几个参数,你形参怎么传参都是那几个参数的,而声明不一样,它要声明出有几个参数且参数是多少,就好比问朋友借钱,先声明我能借钱,才能借钱,而不是我已经买了这个东西再去问朋友借钱的,这是逻辑上顺序的关系。
7、总结
1、半缺省参数必须从右往左依次来给出,不能间隔着给,函数传参必须从左往右传参。
2、缺省函数中的默认参数是不能在定义和声明中同时出现的,只能在声明中出现,不能在定义中出现。
3、缺省值必须是常量或者全局变量
五、函数重载
1、函数重载概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的==形参列表(参数个数或类型或顺序)==必须不同,常用来处理实现功能类似数据类型不同的问题。
2、按照参数类型不同
using namespace std;
int Add(int a, int b)
{
return a + b;
}
double Add(double a, double b)
{
return a + b;
}
int main()
{
Add(1, 1);
Add(1.1, 1.1);
return 0;
}
3、按照参数个数不同
using namespace std;
int Add(int a)
{
return a;
}
int Add(int a, double b)
{
return a + b;
}
int main()
{
Add(1);
Add(1, 1.1);
return 0;
}
4、按照参数顺序不同
using namespace std;
int Add(double a, int b)
{
return a + b;
}
int Add(int a, double b)
{
return a + b;
}
int main()
{
Add(1.1, 1);
Add(1, 1.1);
return 0;
}
5、注意
返回值不同,但参数的个数和类型和顺序都相同则不构成函数重载。
6、C++是为何以及如何实现重载的
先上结论:C语言没有函数重载是因为在所有阶段中用的是函数的名称,走到编译阶段就退出了,因为不影响同名;而C++则是利用相同函数名但不同函数的地址去找函数的定义并将其链接,尤其是在.o符号表中找的是地址,利用地址去找。
四大阶段:
(1)预处理阶段
预处理阶段是将.h文件展开,也就是拷贝到.cpp文件中,其次就是宏替换,条件编译和去掉注释。
(2)编译阶段(单个文件)
1、编译阶段会检查代码的语法是否错误。
2、是将.i进行编译,生成汇编代码以后变成.s。
(3)汇编阶段
.s到.o的操作就是将我们上面的汇编代码转换成CPU所能明白的二进制机器码,而我们是看不懂的,是CPU能看懂的。
(4)链接阶段(兑现承诺)
i、链接的界面化操作
这是连接所作的操作所做的VS界面化操作是:
1、先利用call找jump的地址。
2、再利用jump找到.cpp中的Stack.cpp的栈帧的首地址并进行栈帧的操作。
ii、链接的含义
链接的含义就是:链接就是找到定义(兑现承诺),也就是找符号表中的地址去找定义。当连接器发现Test.o要调用StackPush的时候,发现并没有StackPush的地址,那么就会从Stack.o中生成的符号表中进行寻找地址并将其链接。
Stack.o的符号表:
而C语言太直接了,它在符号表内根本不找地址,找的是函数名!
(5)Linux下C++实现重载逻辑(名字修饰)
在Linux下的C语言与C++调用函数是不一样的,看C语言中的函数名称是func,而在C++中是_Z4funci和_Z4funcid,证明了编译器在编译环节过程中C++环境下编译器是将同名函数但不一样的功能重新命名,进行名字修饰,连接器再根据不同的名字修饰进行寻找函数的。
所以我们之前的vs显示调用函数的关系图即为call的是经过名字修饰过了的函数名。
(6)特殊情况
如果在原本文件中就已经有定义了,那么就不需要链接的过程了,直接在编译阶段进行找地址并进行操作。
六、引用
1、引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间,符号为&。
重要概念:一个人可以有很多个别名,去改变别名则其本身也更改,即:孙悟空的别名是齐天大圣/大师兄等,沙僧说:大师兄,师父被妖怪抓走了,也就是孙悟空知道了师父被妖怪抓走了,也就是说,可以用不同的或者特定的别名。
指针的形象理解:
引用的形象理解:
2、引用的地址演示
3、引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
解释第三点特性:
发现,a的地址和d的地址一样,说明引用一旦引用一个实体,再不能引用其他实体。
4、常引用
关键点在于权限放大、权限平移和权限缩小。
简单赋值判断:
using namespace std;
int main()
{
//不可以
//权限被放大,因为const权限小
const int a = 0;
//int& b = a;
//可以
//c本身就是是赋值给b的
const int c = 0;
int b = c;
//可以
//权限平移或者缩小
int d = 0;
int& f = d;
//加了const就意味着e被限制着,不能进行++,而d没被限制,可以随意++,影响着e的值
const int& e = d;
//可以
//权限平移
const int g = 0;
const int& h = g;
return 0;
}
函数返回值判断:
int func1()
{
static int x = 0;
return x;
}
int& func2()
{
static int x = 0;
return x;
}
int main()
{
int ret = func1(); // 单纯拷贝
//注意:只有设计到引用才有权限的操作
//int& ret1 = func1();// 权限放大
const int& ret2 = func1();// 权限平移
int& ret4 = func2(); // 权限平移
const int& ret5 = func2(); // 权限缩小
return 0;
}
-
5、使用场景
(1)做参数
i、做输出型参数
void Swap1(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int x = 1;
int y = 0;
Swap1(x, y);
return 0;
}
ii、针对大对象/深度拷贝问题 - 提高效率
using namespace std;
struct A { int a[40000]; };
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();
return 0;
}
效率更加高了。
(2)引用做返回值
i、引例
using namespace std;
int Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
演示:
using namespace std;
int Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
我们发现,上面的两种传值返回的函数都是要先生成临时变量,再将返回值存到临时变量最后再进行拷贝的,这种方法如果调用这种函数过于多的话,那么需要存入临时变量过于多,那么所浪费的时间就过于多,所以,我们引出下面的概念:利用引用做返回值能够有效地不使用临时变量,其价值在于减少了拷贝,提高了效率。
ii、正式代码
利用别名不需要开临时空间,只需要是别名即可,即如下图:
(i)错误代码1
先来个错误代码给大家一看:
这串代码在编译器中输出的结果是正确的,但是真的是正确的吗?答案肯定是不是。这里ret单纯是将n进行拷贝赋值,而Count这个函数栈帧销毁以后,看是否清理了栈帧,ret的结果是如上图中表现的。
(ii)错误代码2
再来个错误代码:
(iii)错误代码3
错误代码的延伸:
(iv)正确代码(提高效率)
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
iii、效率比较(针对大对象/深度拷贝问题)
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();
return 0;
}
(3)返回值引用可以修改返回值
为了更好的理解返回值引用可以修改返回值,我们引入C语言来写一个函数:
#include<assert.h>
using namespace std;
typedef struct SeqList
{
int a[100];
int pos;
}SeqList;
int GetSeqList(SeqList* s, int pos)
{
assert(pos >= 0 && pos <= 100);
return s->a[pos];
}
void ModifySeqList(SeqList* s, int pos, int x)
{
assert(pos >= 0 && pos <= 100);
s->a[pos] = x;
}
int main()
{
SeqList s;
ModifySeqList(&s, 0, 1);
cout << GetSeqList(&s, 0) << endl;
return 0;
}
看起来是不是很长,那我们来用C++的引用解决一下:
int& SeqListAt(SeqList& s, int pos)
{
assert(pos >= 0 && pos <= 100);
return s.a[pos];
}
int main()
{
SeqList s;
/*ModifySeqList(&s, 0, 1);
cout << GetSeqList(&s, 0) << endl;*/
SeqListAt(s, 0) = 1;
cout << SeqListAt(s, 0) << endl;
SeqListAt(s, 0) += 5;
cout << SeqListAt(s, 0) << endl;
return 0;
}
实在是太巧妙了,大大节省了代码的长度,从而得出结论——别名具有读写的功能。
(4)总结
三种场景可以使用引用做返回值:
1、大对象
2、用malloc开辟动态数组
3、全局变量
对引用场景做简单总结:
1、基本任何场景都可以用引用传参
2、谨慎用引用做返回值。出了作用域,对象就不在了,就不能用引用返回,还在就可以用引用返回。
(5)加餐
类似于C++写法:
#include<assert.h>
using namespace std;
struct SeqList
{
int a[100];
int pos;
int& at(int pos)
{
assert(pos >= 0 && pos <= 100);
return a[pos];
}
};
int main()
{
SeqList s;
s.at(0) = 0;
s.at(0)++;
cout << s.at(0) << endl;
return 0;
}
operator写法:
#include<assert.h>
using namespace std;
struct SeqList
{
int a[100];
int pos;
int& at(int pos)
{
assert(pos >= 0 && pos <= 100);
return a[pos];
}
int& operator[](int pos)
{
assert(pos >= 0 && pos <= 100);
return a[pos];
}
};
int main()
{
SeqList s;
s.at(0) = 0;
s.at(0)++;
cout << s.at(0) << endl;
s[1] = 10;
s[1]++;
cout << s[1] << endl;
return 0;
}
这些代码后续会进行讲解,这里单纯看一下,不做要求。
6、引用和指针的区别
(1)相同点
相同点:
1、在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
2、在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
简单看图:引用与指针是一样的实现形式。
(2)不同点
- 引用在定义时必须初始化,指针可以不用初始化
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
七、内联函数
1、引例
使用单纯调用函数占用的栈帧过于多,造成很多空间的浪费,所以我们先用宏函数来减少一下调用栈帧的空间:
//宏函数
// 优点:不需要建立栈帧,提高调用效率
// 缺点:复杂,容易出错,代码可读性变差,不能调试
#define Add(x,y) ((x)+(y))
int main()
{
for (int i = 0; i < 10000; i++)
{
cout << Add(i, i + 1) << endl;
}
return 0;
}
当然大家也看到了宏函数的缺点,我们紧接着讲内联函数。
2、概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
结论:调用过于多导致可执行程序变大,那么用户那边的安装包变大,肯定不适用于大众。
3、特性
1、内敛函数适用于短小的频繁调用的函数。
2、inline对于编译器仅仅是一个意见,最终是否成为inline,编译器自己决定(比较长的函数和递归函数编译器本身不会搞内敛,是否内敛取决于编译器)。
3、 inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
当Add函数代码量较少的时候,直接内敛函数,适用于频繁调用函数,所消耗的栈帧量少。
当Add函数代码较多的时候,内敛函数不能进行操作,所以编译器自动调用call去调用函数,不使用内敛函数。
八、auto关键字(C++11)
(1)auto的定义
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
注意点:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译时候会将auto替换为变量实际的类型。
(2)auto的使用细则
i、auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
ii、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
iii、 auto不能推导的场景
(i) auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
(ii) auto不能直接用来声明数组
//错误
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
九、基于范围的for循环(C++11)
1、范围for的语法
我们看一下C++98的做法:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
}
对于有范围的集合而言,因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
}
2、范围for的使用条件
1、for循环迭代的范围必须是确定的。对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
2、迭代的对象要实现++和==的操作。
十、指针空值nullptr(C++11)
1、C++98的坑
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void)的常量*。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
2、nullptr
nullptr就是强转(void*)0。
注意点:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
总结
C++的入门很多也很杂,但并没有展开讲特别多,仅仅是简单地介绍了一下C++的整体内容,也就是将C语言中的弊端给完善一下,处理C语言的弊端并延伸更好的语言。
家人们不要忘记点赞收藏+关注哦!!!