片头
嗨!小伙伴们,大家好!今天我们来一起认识一位新朋友---C++,准备好了么,咱们开始咯~
1.什么是C++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming: 面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序设计语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
2.C++发展史
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称为C with classes。
语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看一下C++的历史版本。
阶段 | 内容 |
C with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数的概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准的第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会的认可,以模仿方式重写C++标准库,引入了STL(标准模板库) |
C++03 | C++标准的第二个版本,语言特性无大改变,主要: 修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份计数报告,正式更名C++0x,即:计划在本世纪第一个10年的某个时间发布 |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对C++11的扩展,主要是修复C++11中的漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等 |
C++17 | 在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等。 |
C++20 | 自C++11以来最大的发行版,引入了许多新的特性,比如: 模块、协程、范围、概念等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等 |
C++23 | 制定ing |
3.C++的重要性
3.1 编程语⾔排⾏榜
![](https://i-blog.csdnimg.cn/direct/ad94338f479c4dfe81bd41532da8ae94.png)
![](https://i-blog.csdnimg.cn/direct/7488869b0dcf4560a7710d0b7e38f4ec.png)
TIOBE编程语言社区排行榜是编程语言流行趋势的一个指标,每月更新,这份排行榜排名基于互联网上有经验的程序员、课程和第三方厂商的数量。排名使用著名的搜索引擎(诸如Google、MSN、Yahoo!、Wikipedia、YouTube以及Baidu等)进行计算。
注意:排名不能说明哪个语言好,哪个不好,每门编程语言都有适应自己的应用场景。
3.2 在工作领域
1.操作系统以及大型系统软件开发
所有操作系统几乎都是C/C++写的,许多大型软件背后几乎都是C++写的,比如:Photoshop、Office、JVM(java虚拟机等),究其原因还是性能高,可以直接操控硬件。
2.服务器端开发
后台开发:主要侧重于业务逻辑的处理,即对于前端请求后端给出对应的响应,现在主流采用java,但内卷化比较严重,大厂可能会有C++后台开发,主要做一些基础组件,中间件、缓存、分布式存储等。服务器端开发比后台开发更广泛,包含后台开发,一般对实时性要求比较高的,比如游戏服务器,流媒体服务器,网络通讯等都采用C++开发的。
3.游戏开发
PC平台几乎所有的游戏都是C++写的,比如:魔兽世界、传奇、CS、跑跑丁车等,市面上相当多的游戏引擎都是基于C++开发的,比如:Cocos2d、虚幻4、DirectX等。三维游戏领域计算量非常庞大,底层的数学全都是矩阵变换,想要画面精美、内容丰富、游戏实时性高,这些高难度需求无疑只能选C++语言。比较知名厂商:腾讯、网易、完美世界、巨人网络等。
4.嵌入式和物联网领域
嵌入式:就是把具有计算能力的主控板嵌入到机器装置或者电子装置的内部,能够控制这些装置。比如:智能手环、摄像头、扫地机器人、智能音响等。
谈到嵌入式开发,大家最能想到的就是单片机开发(即在8位、16位或者32位单片机产品或者裸机上进行的开发),嵌入式开发除了单片机开发以外,还包含在soc片上、系统层面、驱动层面以及应用、中间件层面的开发。
常见的岗位有:嵌入式开发工程师、驱动开发工程师、系统开发工程师、Linux开发工程师、固件开发工程师等。
知名的一些厂商,比如:以华为、vivo、oppo、小米为代表的手机厂;以紫光展锐、乐鑫为代表的芯片厂;以大疆、海康威视、大华、CVTE等具有自己终端业务厂商;以及海尔、海信、格力等传统家电行业。
随着5G的普及,物联网(即万物互联)也成为了一种新兴势力,比如:阿里lot、腾讯lot、京东、百度、美团等都有硬件相关的事业部。
5.数字图像处理
数字图像处理中涉及到大量数学矩阵方面的运算,对CPU能力要求比较高,主要的图像处理算法库和开源库等都是C/C++写的,比如:OpenCV、OpenGL等,大名鼎鼎的Photoshop就是C++写的
6.人工智能
一提到人工智能,大家首先想到的就是Python,认为学习人工智能就要学习Python,这个是误区,python中库比较丰富,使用python可以快速搭建神经网络、填入参数导入数据就可以开始训练模型了。但人工智能背后深度学习算法等核心还是用C++写的。
7.分布式应用
近年来移动互联网的兴起,各应用数据量业务量不断攀升;后端架构要不断提高性能和并发能力才能应对大信息时代的来临。在分布式领域,好些分布式框架、文件系统、中间组件等都是C++开发的。对分布式计算影响极大的Hadoop生态的几个重量级组件:HDFS、zookeeper、HBase等,也都是基于Google用C++实现的GFS、Chubby、BigTable。包括分布式计算框架MapReduce也是Google先用C++实现了一套,之后才有开源的java版本。
除了上述领域外,在:科学计算、浏览器、流媒体开发、网络软件等都是C++比较合适的场景,作为一名老牌语言的常青树,C++一直霸占编程语言前5名,肯定有其存在的价值。
4.C++关键字
C语言有32个关键字,而C++有63个关键字,我们一起来康康~
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_class |
5.命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
比如:
这是为什么呢?
因为,在stdlib.h的头文件中,已经有一个函数名取名为rand了。如果我们将所有的变量名、函数名和类名都存放到全局作用域中,就可能会导致命名冲突,所以C++中出现了命名空间这一概念,要解决这一类问题,我们可以使用namespace关键字来解决。
5.1 命名空间定义
定义命名空间,需要使用namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
还是刚刚的例子,我们可以定义一个命名空间为bit,将rand变量写到命名空间为bit的里面
//bit是命名空间的名字,一般开发中是用项目名字做命名空间名
//1.正常的命名空间定义
namespace bit {
//命名空间中可以定义变量
int rand = 10;
}
命名空间中,除了定义变量,还可以定义函数或者结构体类型
namespace bit {
//命名空间中可以定义函数
int Add(int a, int b) {
return a + b;
}
//命名空间中可以定义结构体
struct Node {
struct Node* next;
int val;
};
}
另外,命名空间还可以嵌套,比如:
namespace N1 { //在命名空间N1中嵌套定义命名空间N2
namespace N2 { //在命名空间N2中定义一个Sub函数
int Sub(int c, int d) {
return c - d;
}
}
}
如果命名空间也同名了怎么办呢?同一个工程中允许存在多个相同名称的命名空间,编译器最后会合并为同一个命名空间中。
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
5.2 命名空间的使用
命名空间中的成员如何使用呢?比如:
namespace bit {
//命名空间中可以定义变量/函数/类型
int a = 0;
int b = 1;
int Add(int left, int right) {
return left + right;
}
struct Node {
struct Node* next;
int val;
};
}
int main() {
//“a”: 未声明的标识符
printf("%d\n", a);
return 0;
}
命名空间的三种使用方式
(1) 指定访问-----加命名空间名称及域作用限定符"::"
像这样,我们就可以访问命名空间域N1里面的a和命名空间域N2里面的b了
小贴士:
如果作用域限定符前面为空的话,访问的是全局变量
此时,我们访问的是main函数中的变量a,如果我们在printf函数里面,加上作用域限定符"::",就可以访问全局变量a了。
(2)指定展开某一个-----使用using将命名空间中的某个成员引入
变量c是命名空间N1的成员,我们可以通过using引入它,所以下面main函数中我们访问变量c就不需要使用作用域限定符了。
(3)全展开-----使用using namespace命名空间引入
像这样,我们使用using namespace引入命名空间N1,所以没有使用作用域限定符就访问到了N1里面的变量a和c,而下面使用了作用域限定符就会访问N2里面的变量b。
总结编译器的默认查找:
a、当前局部域(自留地)
b、全局域(村子野地)
c、到展开的命名空间中查找(相当于张大爷在自己的自留地加了声明,谁需要谁就来拿)
6.C++输入&输出
新生儿会以自己独特的方式向这个崭新的时间打招呼,C++刚出来后,也算是一个新事物,那C++是否也应该向这个美好的世界来声招呼呢?我们一起来看看C++是如何实现问候的。
诶???有点奇怪,如果我们向学习C语言那样只包含一个头文件#include<iostream>的话,系统会提示报错。这是因为,C++标准库做了一件事:为了防止标准库和我们自己定义的变量/函数冲突,所以将标准库里面的函数封装到了一个命名空间std,std是官方库的命名空间。 (简而言之,C++的输入和输出使用到的cin和cout都是放在命名空间std中)
因此,对于上面的报错提示,我们可以有以下几种修改的方式:
(1)指定访问-----加命名空间名称std以及作用域限定符"::"
#include<iostream> //引用C++的头文件
int main() {
// << 流插入
//使用作用域限定符
std::cout << "Hello World!!!" <<std::endl;
return 0;
}
(2)指定展开某一个-----使用using将std命名空间中的cout和endl引入
#include<iostream> //引用C++头文件
using std::cout; //指定展开std中的cout
using std::endl; //指定展开std中的endl
int main() {
// << 流插入
cout << "Hello World!!!" << endl;
return 0;
}
(3)全展开-----使用using namespace命名空间名称std引入
#include<iostream> //引用C++头文件
using namespace std; //使用using namespace std引入
int main() {
// << 流插入
cout << "Hello World!!!" << endl;
return 0;
}
OK,刚刚我们复习了一下学过的知识。现在来正式介绍C++中cout和cin。
举个例子~
#include<iostream> //引用C++头文件
using namespace std; //使用using namespace std引入
int main() {
// << 流插入
int i = 10;
double j = 1.11;
//自动识别类型
cout << i << " " << j << '\n' << endl;
return 0;
}
从以上简短的代码中,我们可以总结如下信息:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含<iostream>头文件以及按命名空间使用方法使用std
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行,它们都包含在<iostream>头文件中
- <<是流插入运算符,>>是流提取运算符
实际上cout和cin分别是ostream和istream类型的对象,而<<和>>在C语言中原本是位运算符,在C++中的含义变了,这里也涉及到运算符重载的知识,在后续我们还会对其深入的学习。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用<iostream>+std的方式。
使用cout和cin的好处在于更方便,不需要像printf和scanf一样手动控制格式
缺点在于打印一串数据的情况比较复杂
小贴士:
关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整型输出进制格式等等。因为C++兼容C语言的用法,这些有不怎么常用,我们到这里就不展开学习了。
std命名空间的使用惯例:
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以在项目开发中不推荐使用全局展开,我们可以指定命名空间访问,例如using std::cout和using std::cin来展开常用的命名空间成员。
7.缺省参数
7.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺失值,否则使用指定的实参。
#include<iostream> //引入C++的头文件
using namespace std; //展开命名空间std
void Func(int a = 0) {
cout << a << endl;
}
int main() {
Func(); //没有传参时,使用参数的默认值
Func(10); //传参时,使用指定的实参
}
7.2 缺省参数分类
- 全缺省参数
//全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
- 半缺省参数
//半缺省参数
void Func(int a, int b = 10, int c = 20) {
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
关于缺省参数,我们需要注意几点:
(1)半缺省参数必须从右到左依次给出,不能间隔着给
(2)缺省参数不能同时出现在函数的定义和声明中
why?因为函数的定义和声明中同时出现缺省参数,而两个位置设置的值不同,编译器就无法确定该使用哪个缺省值
所以,缺省参数在公共头文件包含的函数声明中指定,不要在函数的定义中指出(如果在函数的定义中指定缺省参数值,在公共头文件包含的函数声明中不能再次指定缺省参数值)
(3)缺省值必须是常量或者全局变量
(4)C语言不支持(编译器不支持)
8.函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,中国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
8.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
例如我们以前在C语言中想实现Add函数,但是int和double类型的数据不能用同一个函数处理,每处理一种类型的数据就要写一个函数,函数间还不能同名
但是在C++中针对这个问题进行了优化,编译器会有一套自己的函数名修饰规则来修饰不同形参个数/类型/类型顺序的同名函数
#include<iostream>
using namespace std;
//1.参数类型不同
int Add(int a, int b) {
cout << "int Add(int a, int b) " << endl;
return a + b;
}
double Add(double a, double b) {
cout << "double Add(double a, double b)" << endl;
return a + b;
}
//2.参数个数不同
void f() {
cout << "f()" << endl;
}
void f(int a) {
cout << "f(int a)" << endl;
}
//3.参数类型顺序不同
void f(int a, char b) {
cout << "f(int a, char b)" << endl;
}
void f(char b, int a) {
cout << "f(char b, int a)" << endl;
}
int main() {
Add(10, 20);
Add(10.8, 20.5);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
8.2 C++支持函数重载的原理---名字修饰(name Mangling)
为什么C++支持函数重载呢?名字修饰(name Mangling)
名字修饰是一种在编译过程中,将函数、变量的名称重新改编的机制。简单来说就是编译器为了区分多个同名函数,规定了一个新的规则来对原本的名字进行修饰
为什么C语言不支持函数重载,是因为它的名字修饰规则过于简单,只是在函数名前面添加了下划线
小贴士:在C++的函数前加上extern"C"就可以让函数按照C语言的风格编译
这里可以看到,Add函数按照C语言的风格编译后名字变成了_Add
而在C++中,修饰规则得到了完善,所以可以支持函数重载
不过,不同的编译器有自己的函数名修饰规则,上面的就是在Windows下的vs的修饰规则,有点难理解,咱们一起来康康~
接下来我们展示g++的修饰规则,它会比前者更加的简单易懂
#include<iostream>
using namespace std;
int Add(int x, int y) {
return x + y;
}
double Add(double x, double y) {
return x + y;
}
int main() {
Add(1, 2);
Add(1.5, 2.6);
return 0;
}
上面是源文件代码,我们在终端中输入"g++ -S test.cpp -o test.s" 来查看其汇编代码
可以看到,这两个就是上面int类型的Add函数和double类型的Add函数重载后的名字了。
其中,_Z后跟着的数字就是原函数名的长度,后面的"ii"和"dd"就是参数的类型。
通过这些,我们就能理解为什么C语言不支持函数重载,而C++通过函数名的修饰规则可以区分同名函数了,只要参数个数/类型/类型顺序不同,修饰后的名字就不同,也就可以区分了。
注意: 如果两个函数的函数名和参数个数/类型/类型顺序都相同,返回值不同是不构成重载的,因为调用时编译器没办法区分。
9.引用
9.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为“铁牛”,江湖上人称“黑旋风”
引用的符号: &,用法: 类型& 引用变量名(对象名)= 引用实体;
注意:引用类型必须和引用实体是同种类型的
引用变量也可以作为被引用的对象
那如果我们想实现2个数交换,该怎么做呢?
简单回顾一下:
#include<iostream>
using namespace std;
void Swap(int* a, int* b)
//形参是实参的一份临时拷贝,修改形参不会影响实参
//因此需要传地址
{
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 0, y = 1;
Swap(&x, &y);
cout << "x = " << x << ",y = " << y << endl;
return 0;
}
现在我们需要使用引用符号&来实现2个数交换,此时形参是实参的别名
当然啦,指针也可以进行引用
9.2 引用特性
(1)引用在定义时必须初始化
(2)一个变量可以有多个引用,也就是取多个别名
(3)一旦引用变量已经引用过了某个对象,就再也不能引用其他对象
通过上面的例子,我们可以看出,引用和指针还是有区别的。链表这些地方我们使用指针会更方便;在一些输出型参数,也就是形参的改变要影响实参的地方,我们可以选择引用。
9.3 常引用
(1)如果被引用的对象被const修饰,而引用变量没有被const修饰会报错
(2) 引用变量如果没有被const修饰,不能将常量作为引用对象
(3)引用变量没有被const修饰且和被引用对象不是同一类型时会报错
9.4 引用的使用场景
(1)引用作为函数参数,就是上面提到过的Swap
void Swap(int& a,int& b){
int temp = &a;
&a = &b;
&b = temp;
}
(2)引用作为函数返回值
int& Add(int a, int b) {
int c = a + b;
return c;
}
int main() {
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1,2) is :" << ret << endl;
cout << "Add(1,2) is :" << ret << endl;
return 0;
}
上面的代码会输出什么结果?
事实上,结果是未定义的。
第一次调用Add函数的时候,函数栈帧创建完毕,局部变量c(此时值为3) 保存在Add的栈帧中。函数运行结束后,栈帧销毁,内存空间被系统回收,此时变量c已经没有任何意义了,所以ret引用了一块已经被释放的空间。
第二次调用Add函数和第一次一样,只不过局部变量c的值变成了7。
注意:虽然空间被回收,里面的东西却都还在。就像去住酒店,退房后里面的东西在没打扫前都是一直保持原样的,而不是说退房后里面的东西都消失了。
所以,当我们第一次输出ret的值是7,第二次就变为了随机值
因此,如果出了函数作用域后返回对象没有销毁(static,malloc等),则可以使用引用返回,否则必须使用传值返回
另外的,引用返回还可以修改返回的对象
#include<iostream>
using namespace std;
int& Func(int* a, int i) {
return a[i];
}
int main() {
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++) {
Func(a, i) = i * 10;
cout << a[i] << " ";
}
return 0;
}
9.5 传值和引用返回的效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
(简单说明:以值作为函数参数或者返回值类型的时候,函数并不会直接传递实参或者将变量本身直接返回,而是会创建一个临时变量作为“中间商”,因此会影响效率)我们以传值返回为例测试一下:
#include<iostream>
#include<time.h>
using namespace std;
struct A {
int a[10000];
};
A a;
//值返回
A TestFunc1() {
return a;
}
//引用返回
A& TestFunc2() {
return a;
}
int main()
{
//以值作为函数的返回值类型
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;
return 0;
}
可以看到,传值返回比引用返回效率低了很多
9.6 引用和指针的区别
在语法概念上,引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
#include<iostream>
using namespace std;
int main() {
int a = 10;
//ra相当于是a的别名
int& ra = a;
cout << "&a = " << &a << endl;
cout << "&ra = " << &ra << endl;
return 0;
}
在底层实现上实际是有空间的,因为引用是按照指针的方式来实现的。
#include<iostream>
using namespace std;
int main() {
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
我们来看一下引用和指针的汇编代码对比:
二者的不同点在于:
- 从概念上来说,引用是定义了一个变量的别名,指针是存储了变量的地址
- 引用在定义时必须初始化,指针可以不用
- 引用在初始化时引用了一个对象后,就不能再引用其他对象,而指针可以随时改变指向
- 没有空引用,但是有空指针
- 从sizeof来说:引用的sizeof结果是引用类型的大小,但指针始终是地址所占字节个数
- 引用的++是被引用的对象+1,而指针++是指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问对象的方式不同,指针需要解引用,而引用是由编译器自己处理
- 引用比指针使用起来更安全
那么,引用可以完全替代指针吗?不行!因为如果是链表,进行增删查改操作时,需要改变节点的指向,引用不能改变节点的指向。因此,引用不能完全替代指针。
拓展:
如果我们需要频繁使用小函数(例如:Add函数,Swap函数),我们可以选择宏函数。
我们先来示范一下Add函数
// C->宏函数
#define Add(a,b)((a)+(b))
Q1:为什么不能加分号?
Q2: 为什么要加里面的括号?
Q3: 为什么要加外面的括号?
解决方案:
A1:如果加了分号,就会变成这样
A2:之所以加上里面的括号,是因为加号的优先级排在按位与&和按位或|的前面,如果表达式a和表达式b中含有&或者|,则会优先计算 a+b。所以必须加上里面的括号。
A3:之所以加上外面的括号,因为“*”和“/”的优先级比“+”高,如果出现“*”或者“/”,则会优先计算“*”或“/”。为了避免这种情况,我们需要加上外面的括号。
欧克克,回归正传,我们学习下一个知识点---内联函数
10.内联函数
10.1 内联函数的概念
在C++中,为了解决一些频繁调用小函数大量消耗栈内存的问题,引入了inline修饰符。
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开函数,不用调用函数建立栈帧。
其作用很像宏函数,不过相比宏函数的特点,它可以调试,有类型的检查,不容易出错。
//内联函数
inline int Add(int x, int y) {
return x + y;
}
inline是一种比宏更简单更好的方式,而且不降低效率。
如果不用inline修饰,在汇编代码中我们会看到call指令去调用函数的操作
但是在Add函数前增加inline将其修改为内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
1.在release模式下,查看编译器生成的汇编代码中是否存在call Add
2.在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化)
在项目中点击属性
在属性中修改这两项
然后我们在debug模式下查看内联函数的汇编代码
可以看到,已经没有call指令了。
10.2 特性
1.inline是一种以空间换时间的做法,这里的空间指编译出来的可执行文件的大小,内联函数会直接在程序中展开,越长的函数调用越多次就会使文件大小暴增。
如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
tip:内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
假如有一个1000行的函数,我们要在程序中调用1000次,如果不用inline修饰的话就是1000+1000行代码,如果用inline修饰的话,就是1000*1000行代码,你觉得哪个更好?
可以看到,太长的函数尽管用了inline修饰,编译器也会忽略掉,选择call调用函数
一般建议将函数规模较小、非递归且调用次数较多的函数使用inline修饰
需要注意的是,inline不建议声明和定义分离,会导致链接错误,对于内联函数最好放在头文件中定义
此时在test.cpp中,编译时展开了F.h的内容,而里面只有f函数的声明,只能等链接的时候在符号表中寻找对应函数,也就是通过call指令调用函数,而内联函数是不会生成call指令的。
改进方法:在头文件中直接定义内联函数!
11.auto关键字(C++11)
随着我们不断深入学习C++,程序越来越复杂,使用的类型也会越来越复杂,不仅难于拼写,还容易出错。
除了typedef,我们还有另一个选择是auto,它可以帮我们自动推导类型。
需要注意的是,使用auto变量时必须对变量进行初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。
因此,auto并非是一种类型的声明,而是一个占位符,编译器在编译时会将auto替换为变量实际的类型。
11.1 使用auto的注意事项
(1)如果auto后加上*就限定了赋值的对象必须是指针
(2) 我们使用auto时,也可以在同一行定义多个变量,前提是这些变量必须是相同的类型
(3) 用auto声明引用类型还是要加上&符号的
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;//b相当于是a的别名
return 0;
}
11.2 不能使用auto的场景
(1)auto不能作为函数的参数
(2)auto不能用于声明数组
12.范围for(C++11)
现代C++倾向于让各种繁杂的操作变得简洁,因此诞生了许多语法糖,范围for算是其中的典型。
在C++98/03中,不同的容器和数组遍历的方式有很多,不够统一,也不够简洁。
而C++出现了基于范围的for循环,可以更简洁的去遍历容器和数组,也更方便我们使用了。
以前我们遍历数组的方式如下:
#include<iostream>
using namespace std;
int main() {
int a1[] = { 1,2,3,4,5 };
int n1 = sizeof(a1) / sizeof(int);
for (int i = 0; i < n1; i++) {
cout << a1[i] << " ";
}
cout << endl;
return 0;
}
对于一个有范围的集合而言,由程序员来声明循环的范围未免太多余,还容易出错。接下来我们用范围for遍历数组:
for循环的括号中由冒号“:” 分成两部分,左边是范围内用于迭代的变量,右边表示被迭代的范围
这里也用到了前面的auto关键字,如果我们想对范围内的元素进行修改,还可以用到引用&
和普通循环一样,范围for中也可以使用continue和break。
注意:范围for迭代的范围必须是确定的。对于数组而言,就是数组第一个元素到最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环的迭代范围。
像这种情况就不能使用范围for,因为传参到函数中时传递的不是一整个数组而是数组指针,此时for的范围不确定。
13.指针空值nullptr(C++11)
以前,我们给一个没有指向的指针进行初始化的时候会使用NULL,而NULL实际上是一个宏。
我们在C语言中使用NULL木有问题,但是在C++中就会出现问题,为什么呢?
在传统的C头文件stddef.h中,可以看到如下代码:
#ifndef NULL
#ifdef _cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,在C++中NULL被定义为0,这样会造成什么麻烦呢?
可以看到,就算传递的参数为NULL,程序还是会调用int类型的Func,而不是int*类型的Func,这违背了我们的目的。
因此出现了指针空值nullptr来填补这个bug,使用nullptr时不需要包含头文件,因为它是C++作为新关键字引入的。为了提高代码的健壮性,我们后续表示指针空值时最好都使用nullptr。
可能有人会问:为啥不直接把这个bug修复了呢?因为语言有一个向前兼容的原则,也就是已经出现的东西即使有问题也不能修改。如果贸然去修改了可能会导致以前的代码无法运行,可能造成巨大的损失。
片尾
今天我们学习了C++入门知识,本篇有点长,希望看完这篇文章能对友友们有所帮助!!!
求点赞收藏加关注!!!
谢谢大家!!!