初识C++

片头

嗨!小伙伴们,大家好!今天我们来一起认识一位新朋友---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++98C++标准的第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会的认可,以模仿方式重写C++标准库,引入了STL(标准模板库)
C++03C++标准的第二个版本,语言特性无大改变,主要: 修订错误、减少多异性
C++05C++标准委员会发布了一份计数报告,正式更名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 编程语⾔排⾏榜
  TIOBE排⾏榜是根据互联⽹上有经验的程序员、课程和第三⽅⼚商的数量,并使⽤搜索引擎(如
Google、Bing、Yahoo!)以及Wikipedia、Amazon、YouTube和Baidu(百度)统计出排名数据,只是反映某个编程语⾔的热⻔程度,并不能说明⼀⻔编程语⾔好不好,或者⼀⻔语⾔所编写的代码数量多少。
  2024年8⽉TIOBE发布的编程语⾔排⾏榜
 ​​​​​​

  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个关键字,我们一起来康康~

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_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++入门知识,本篇有点长,希望看完这篇文章能对友友们有所帮助!!!

点赞收藏加关注!!!

谢谢大家!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值