河流之所以能够到达目的地,是因为它懂得怎样避开障碍。💓💓💓
✨说在前面
亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇目我们已经完结了初阶数据结构部分的学习,**从这一章节开始,我们要开始学习一门新的编程语言---C++。**虽然说C++有些难度,但有了之前C语言的基础,相信大家还是非常有把握能够把它学好的。
**今天我们将要学习C++的基础知识,**包含C++的历史、命名空间、输入输出、缺省参数等等知识,我们今天就解开C++神秘的面纱,详细剖析这一门新的编程语言吧~
博主主页传送门:愿天垂怜的博客
🍋知识点一:C++的发展历史
• 🌰1.C++发展历史
1. 起源与早期发展
- 起源:C++起源于20世纪80年代,由丹麦计算机科学家本贾尼·斯特劳斯特鲁普(Bjarne Stroustrup)在AT&T贝尔实验室发起。当时,斯特劳斯特鲁普为了解决C语言的不足,开始设计一种新的编程语言,这就是C++的雏形。他最初称之为"C with Classes",意为带有类的C语言。
- 正式命名:1983年,这种语言被正式命名为C++,以表示这是C语言的一种进化和扩展。
2. 标准化进程
- 标准化开始:C++的标准化工作于1989年开始,并成立了一个ANSI和ISO(国际标准化组织)的联合标准化委员会。
- 标准发布:
- 1994年1月25日,联合标准化委员会提出了第一个标准化草案,并在保持斯特劳斯特鲁普最初定义的所有特征的同时,增加了部分新特征。
- 1998年,第一个C++标准(C++98)发布,其中包含了许多重要的特性,如类、继承、模板和异常处理等。
- 随后,C++的标准化工作继续推进,相继发布了C++03、C++11、C++14、C++17和C++20等多个版本。每个版本都引入了新的特性和改进,使得C++成为一门功能丰富的编程语言。
3. 重要里程碑
- 编译器问世:1985年,第一个可用的C++编译器问世,这标志着C++从理论走向了实践。
- 经典著作:斯特劳斯特鲁普在C++的发展过程中撰写了多部经典著作,如《The C++ Programming Language》,这些著作对C++的推广和普及起到了重要作用。
- 商业应用:随着C++的不断发展,越来越多的商业软件开始使用C++编写,如Windows操作系统、浏览器IE等。
4. 未来展望
C++的未来发展方向包括进一步提高语言的性能、简化语法、增加并行和异步编程的支持,并继续引入更多现代编程范式的特性。随着技术的不断进步和需求的不断变化,C++将继续保持其强大的生命力和广泛的应用前景。
5. 关键人物
- 本贾尼·斯特劳斯特鲁普:作为C++的设计者和实现者,斯特劳斯特鲁普对C++的发展做出了巨大贡献。他的工作不仅推动了C++的标准化进程,还促进了C++在各个领域的应用。
总结:C++的发展历史是一个不断进化和扩展的过程。从最初的"C with Classes"到如今的C++20,C++已经发展成为一门功能强大、应用广泛的编程语言。
• 🌰2.C++的迭代与更新
|-----------|-----------|--------------------------------------------------------------------------------------------|
| 时间 | 阶段 | 内容 |
| 1998年 | C++98 | C++官方的第一个版本,接大多数编译器都支持,得到了国际标准化组织(ISO)和协会认可,以模版方式重写C++的标准库,引入了STL(标准模版库)。 |
| 2003年 | C++03 | 这是C++标准的一个重大修订,主要聚焦了语言的稳定性和兼容性。C++03修正了C++98中的错误和漏洞,同时引入了一些新的特性和功能,如tr1库。 |
| 2011年 | C++11 | 这是一次革命性的更新,增加了大量的新特性和功能,使得C++更像一种新语言,增加了lanmbda、范围for、右值引用和移动语义、边长模板参数、STL容器和核心能指针、标准线程库等。 |
| 2014年 | C++14 | 对C++11的扩展,主要是修复C++11中的漏洞以及改进,比如:泛型的lambda的返回值类型推导,二进制字面常量等。 |
| 2017年 | C++17 | C++17进一步增强了C++的功能和表达能力。这次更新引入了if constexpr等,同时改进了标准库中的多个组件,如string等。 |
| 2020年 | C++20 | C++20是C++历史上的有一个重要里程碑。这次更新引入了一系列新特性和改进(Coroutines)、概念(Concepts)、模块化(Modules)等。 |
| 2023年 | C++23 | C++23是一个小版本更新,进一步完善和改进现有特性,增加了if consteval、falt_map、import std导入标准库等。 |
| 2026年 | C++26 | 制定中... |
• 🌰3.编程语言排行
• 🌰4.C++在工作领域的应用
C++的应用领域服务器端、游戏(引擎)、机器学习引擎、音视频处理、嵌入式软件、电信设备、金融应用、基础库、操作系统、编译器、基础架构、基础工具、硬件交互等很多方面都有。
1.大型系统软件开发
如编译器、数据库、操作系统、浏览器等等
2.音视频处理
常见的音视频开源库和方案有FFmpeg、WebRTC、Mediasoup、ijkplayer,音视频开发最主要的技术栈就是C++。
3.PC客户端开发
一般是开发Windows上的桌面软件,比如WPS之类的,技术栈的话一般用C++和QT,QT是一个跨平台的C++图形用户界面(Graphical User Interface,GUI)程序。
4. 服务端开发
各种大型应用网络连接的高并发后台服务。这块Java也比较多,C++主要用于一些对新能要求比较高的地方。如:游戏服务、流媒体服务、量化高频交易服务等。
5. 游戏引擎开发
很多游戏引擎就都是使用C++开发的,游戏开发要掌握C++基础和数据结构,学习图形学知识,掌握游戏引擎和框架,了解引擎实现,引擎源代码可以学习UE4、Cocos2d-x等开源引擎实现。
6. 嵌入式开发
嵌入式把具有计算能力的主控板嵌入到机器装置或者电子装置的内部,通过软件能够控制这些装置。比如:智能手环、摄像头、扫地机器人、智能音响、门禁系统、车载系统等等,粗略一点,嵌入式开发主要分为嵌入式应用和嵌入式驱动开发。
7. 机器学习引擎
机器学习底层的很多算法都是用C++实现的,上层用python封装起来。如果你只想准备数据训练模型,那么学会python基本上就够用了,如果你想做机器学习系统的开发,那么需要学会C++。
- 测试开发/测试
每个公司研发团队,有研发就有测试,测试主要分为测试开发和功能测试,测试开发一般是使用一些测试工具(selenium、Jmeter等),设计测试用例,然后写一些脚本进行自动化测试,性能测试等,有些还需要自行开发一些测试用具。功能测试主要是根据产品的功能,设计测试用例,然后手动的方式进行测试。
• 🌰5.C++参考文档
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/
**说明:**第一个链接不是C++的官方文档,标准也只更新到C++11,但是以头文件的形式呈现,内容比较易看、好懂。后面两个链接分别是C++官方文档的中文版和英文版,信息和全,更新到了最新的C++标准,但是相比第一个不那么易看;几个文档各有优势,可以结合使用。
🍋知识点二:命名空间
• 🌰1.namespace的价值
在C/C++中,变量、函数和后面要学习的类都是大量存在的,这些变量、函数和类的别称都存在于全局作用域中, 可能会导致很多冲突。使用命名空间的目的就是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
C语言项目类似下面这样的命名冲突是普遍存在的问题,C++引入namespace就是为了更好地解决这样的问题。
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
//编译报错:error C2365:"rand" : 重定义;以前的定义是"函数"
return 0;
}
由于rand函数实际上在<stdlib,h>头文件中已经被定义为一个函数,用于生成伪随机数,且代码中定义了同名的变量rand,这导致了**命名冲突,**编译器无法区分到底是想调用rand函数还是引用自己定义的变量rand。
• 🌰2.namespace的定义
🔥定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{ }即可,{ }中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
🔥namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不在冲突了。
🔥C++中的域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量的声明周期。
🔥namespace只能定义在全局,当然他还可以嵌套定义。
🔥项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
🔥C++标准库都放在一个叫std(standard)的命名空间中。
• 🌰3.命名空间的使用
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找,所以我们要使用命名空间中定义的变量/函数,有三种方式:
🔥指定命名空间访问,项目中推荐这种方式。
🔥using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
🔥展开命名空间中的全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
cpp
#include <stdio.h>
#include <stdlib.h>
namespace Lilt1
{
int a = 10;
int b = 20;
}
namespace Lilt2
{
int e = 50;
namespace Lilt3
{
int c = 0;
int d = 30;
}
}
//展开命名空间Lilt1
using namespace Lilt1;
//部分展开Lilt2中的变量e
using Lilt2::e;
int main()
{
printf("%d\n", a);
printf("%d\n", e);
printf("%d\n", Lilt2::Lilt3::d);
return 0;
}
🍋知识点三:输入、输出
🔥<iostream>是Input Output Stream的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
🔥std::cin是istream类的对象,它主要面向窄字符(narrow characters(of type char))的标准输入流。
🔥std::endl是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
🔥<<是流插入运算符,>>是流提取运算符(C语言还用这两个运算符做位运算符左移/右移)。
🔥使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是用过函数重载实现的),其实最重要的是C++的流可以更好的支持自定义类型对象的输入输出。
🔥IO流涉及类和对象、运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识一下C++ IO流的用法,之后会有一个站街专门细节讲解IO流库。
🔥cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们。
🔥一般日常练习中我们可以用using namespace std,实际项目开发中不建议。
🔥这里我们没有包含<stdio.h>,也可以使用printf和scanf,在包含<iostream>间接包含了。VS系列编译器是这样的,其他编译器不确定。
cpp
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
cout << a << " " << b << "\n" << c << "\n" << endl;
std::cout << a << " " << b << " " << c << std::endl;
int x, y, z = 0;
cin >> x >> y >> z;
cout << x << " " << y << " " << z << endl;
return 0;
}
注意:
cpp
int main()
{
// 在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下3行代码
// 可以提高C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
🍋知识点四:缺省参数
• 🌰1.什么是缺省参数?
🔥缺省参数是声明或定义时为函数的参数指定一个缺省值。**在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参,**缺省参数分为全缺省和半缺省参数(有些地方把缺省参数也叫做默认参数)。
🔥全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
🔥带缺省参数的函数调用,C++规定必须从左往右依次给实参,不能跳跃给实参。
🔥函数声明和定义分离时,缺省参数不能再函数声明和定义中同时出现,规定必须在函数声明给缺省值。
cpp
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func();//没有传参时,使用参数的默认值
Func(10);//传参时,使用指定的实参
/*Func1();
Func1(1);
Func1(1, 2);
Func1(1, 2, 3);*/
Func2(100);
Func2(100, 200);
Func2(100, 200, 300);
return 0;
}
缺省参数的使用案例:
cpp
#include <iostream>
using namespace std;
#include "Stack.h"
//栈的初始化
void STInit(ST* pst, int n)
{
assert(pst);
pst->a = (STDataType*)malloc(n * sizeof(STDataType));
pst->top = pst->capacity = 0;
}
int main()
{
//确定知道要插入1000个数据,初始化时一并开好,避免扩容
ST s2;
STInit(&s2, 1000);
for (int i = 0; i < 1000; i++)
{
STPush(&s1, i);
}
return 0;
}
• 🌰2.函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++调用就表现出了多态行为,使用更加灵活。C语言是不支持同一作用域中出现同名函数的。
cpp
#include <iostream>
using namespace std;
//参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
//参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
//参数顺序不同
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(1, 2);
Add(1.1, 2.2);
f();
f(1);
f(1, 'x');
f('x', 1);
return 0;
}
注意下列两种情况:
cpp
//是否为函数重载?
//构成,但是f()调用时,会报错,存在歧义,编译器不知道调用谁
namespace bit1
{
void f1()
{
cout << "f1()" << endl;
}
}
void f1(int a = 10)
{
cout << "void f1(int a = 10)" << endl;
}
//是否为函数重载?
//不构成,返回值不能作为重载的条件
void fxx() {}
int fxx() { return 0; }
🍋知识点五:引用
• 🌰1.什么是引用?
引用不是新定义一个变量,而是给已经存在的变量取了一个别名,编译器不会为应用变量开辟内存空间,它和它应用的变量共用同一块空间。比如:水浒传中李逵,宋江叫"铁牛",江湖人称"黑旋风";林冲,外号"豹子头";
类型& 引用别名 = 引用对象
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<<和>>,这里引用也和取地址使用了同一个符号&,大家注意使用方法角度区分就可以。
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 0;
//引用:b和c是a的别名
int& b = a;
int& c = a;
//也可以给别名b取名,d相当于还是a的别名
int& d = b;
//这里取地址我们看到的是一样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
cout << a << endl;
//d++全部abc++
d++;
cout << a << endl;
return 0;
}
• 🌰2.引用的特性
🔥引用在定义时必须初始化
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
//编译报错:"ra": 必须初始化引用
int& ra;
return 0;
}
🔥一个变量可以有多个引用
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 0;
//引用:b和c是a的别名
int& b = a;
int& c = a;
//也可以给别名b取名,d相当于还是a的别名
int& d = b;
return 0;
}
🔥引用一旦引用一个实体,再不能引用其他实体
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int c = 20;
/ 这里并非让b引用c,因为C++引用不能改变指向,
//这里是⼀个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
• 🌰3.引用的使用
🔥引用对象在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
🔥引用传参跟指针传参的功能是类似的,引用传参相对更方便一些。
🔥引用返回值的场景相对比较复杂,我们再这里简单讲了一下场景,后续内容在类和对象章节细细道来。
🔥引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++中的引用和其他语言的引用(如Java)是由很大区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。
🔥一些主要用C代码实现版本数据结构的教材中,**使用C++引用替代指针传参,目的是简化程序,避免复杂的指针,**但是很多同学没有学习过引用,导致一头雾水。
示例1:交换函数不需要再传地址
cpp
void Swap(int& rx, int& ry)
{
int temp = rx;
rx = ry;
ry = temp;
}
int main()
{
int x = 0;
int y = 2;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
在我们以前学习C语言学习指针的时候,往往会举到这个例子,就是两数互换。因为我们知道,**形参是实参的一份临时拷贝,对形参的修改不会影响到实参。**所以,当我们想要修改传过去的参数时,应该传参数的地址,利用指针的解引用修改其对应的值。
但是现在,我们有了引用。由于引用的别名和原先的变量都是同一块空间,我们对一个变量的别名进行修改,被引用对象也会被修改。所以,我们直接传变量的别名,就可以实现不需要指针而在改变引用对象时同时改变被引用对象。
示例2:数据结构中用引用代替指针传参
cpp
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, * PNode;
//指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
//这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
和示例1一样,在部分数据结构教材上,使用引用代替指针的写法。但是需要注意,**引用不能叠加,没有二级引用这样的写法,**所以我们不能写LTNode&& phead,而要写LTNode*& phead。
示例3:引用做返回值
cpp
typedef int STDataType;
typedef struct stack
{
STDataType* a;
int top;
int capacity;
}Stack;
//弹栈
void STPush(Stack& rs, STDataType x);
//初始化栈
void STInit(Stack& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
//取栈顶数据
int& STTop(stack& rs)
{
assert(rs.top > 0);
return rs.a[rs.top];
}
int main()
{
// 调用全局的
stack st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
cout << STTop(st1) << endl;
//取得栈顶元素的同时还可以对栈顶元素进行操作
STTop(st1) += 10;
cout << STTop(st1) << endl;
return 0;
}
在示例3中,我们利用STTop函数直接修改了栈顶元素的值。如果我们不用int&做返回值,直接用int做返回值类型,是没有办法实现这个功能的。因为传值返回return语句中返回值会被存储在一个临时变量中,也就是返回它的**拷贝,而这个临时变量具有常属性,**无法被修改。而我们用引用作为返回值,直接返回栈顶元素的别名,没有中间的临时变量,所以可以直接通过修改别名来修改栈顶元素的值,更加地方便。
• 🌰4.const引用
🔥可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但不能放大。
🔥需要注意的是类似 int& rb = a * 3; double d = 12.34 ; int& rd = d; 这样的一些场景下a * 3的结果保存在一个临时对象中,int& rd = d也是类似,在类型转换中会产生临时对象存储中间值,也就是说,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
🔥所谓临时对象就是**编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象。**C++中把这个未命名对象叫做临时对象。
cpp
#include<iostream>
using namespace std;
int main()
{
const int a = 10;
//int& ra = a;
//权限被放大 error
const int& ra = a;
int b = 20;
const int& rb = b;
//权限被缩小 ok
return 0;
}
上面代码,a是const常量,那么a的别名ra就必须用const int& ra而不能int& ra,否则**权限就被放大,这是不行的;相反,b是int类型的变量,可以用int& rb,可以用const int& rb,如果是后者,那么权限就被缩小,**这是可以的。
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
const int& rc = (a + b);
//(a + b)的值存在临时对象中,临时对象具有常属性,必须用const引用
double d = 12.34;
int i = d;
//隐式类型转化也会创建临时变量
const int& ri = d;
double& rd = d;
return 0;
}
上面的代码中,rc引用(a + b)的和结果。对于**表达式的结果,会被存储在临时对象中,具有常属性,必须用const引用;临时对象还有一种情况,就是隐式类型转换,**在隐式类型转换的同时变量的值也会被存储在临时对象中,也必须用const引用。
• 🌰5.指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各自有自己的特点,互相不可替代。
🔥语法概念上引用是一个变量的取别名,不开空间;指针是存储一个变量的地址,需要开空间。
🔥**引用在定义时必须初始化,指针建议初始化,**但是在语法上不是必须的。
🔥引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
🔥sizeof中含义不同,引用结果为引用类型的大小,但是指针始终是地址空间所占字节个数(3位平台下占4个字节,64位平台下占8个字节)。
🔥指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来会相对安全一些。
注意:引用和指针在底层是一样的:
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
//指针
int* pa = &a;
*pa = 20;
//引用
int& ra = a;
ra = 30;
return 0;
}
转到反汇编:
可以发现,指针和引用的汇编代码是一样的,他们在底层是相同的。但是我们在使用上不需要考虑底层,明确引用是不开空间,指针是开空间的,不要混淆。
🍋知识点六:内联函数和空指针
• 🌰1.inline与内联函数
🔥用inline修饰的函数叫做**内联函数,**编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率。
🔥inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。**inline使用与频繁调用的短小函数,**对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
🔥C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂也很容易出错,且不方便调试,C++设计了inline目的就是替代C的宏函数。
🔥inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
🔥VS编译器debug版本下面默认是不展开inline的,这样方便调试,debug版本想要展开需要设置以下两个地方:
为了体现C宏函数的麻烦,以实现ADD加法宏函数为例:
cpp
//实现⼀个ADD宏函数的常⻅问题
#define ADD(int a, int b) return a + b;
#define ADD(a, b) a + b;
#define ADD(a, b) (a + b)
上面三种写法都是错误的,正确的宏函数的写法如下:
cpp
#define ADD(a, b) ((a) + (b))
那么,为什么不能加分号?为什么要加外面的括号?又为什么要加里面的括号?
当我们加了分号,则:
**cout << ADD(1, 2) << endl;**会被替换为 **cout << ((1) + (2)); << endl;**这会导致输出语句错误,因为多了一个;
如果我们没有加外面的括号,则:
**int ret = ADD(1,2) * 5;**就会被替换为 **int ret = (1) + (2) * 5;**原本我们希望得到3 * 5的值15,而没有加外面的括号导致2先和5乘,运算顺序发生了改变。
如果我们没有加里面的括号,则:
**int ret = ADD(1 & 2, 2 | 3);就会被替换成int ret = (1 & 2 + 2 | 3);**而 & 和 | 的优先级(位运算符的优先级)是没有 + 来得高的,所以2就会先加2变成4,和我们希望的结果又不一样了。
由此可见,**宏函数是很麻烦的,**稍不留神就会导致结果不符合我们的预期,而且还不报错,当代码一长,不好找错误的地方在哪。
为了解决这个问题,我们使用了内联函数:
cpp
#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
return ret;
}
int main()
{
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
cout << ret << endl;
return 0;
}
当我们再ADD前面加上关键字inline,此时ADD函数就是一个**内联函数,**它和宏类似地,**在函数调用的位置直接展开。**对于一般的函数,编译后的指令存储在代码段中,而函数中的参数和局部变量存储在函数的栈帧空间。在执行call指令时,会跳转到该函数的地址,执行函数体内部的指令。而对于内联函数,由于直接插入在调用的位置,那么它的参数和变量(如果有的话)将使用调用它的函数的栈帧,不需要创建函数栈帧,也就节省了空间,提升了效率。
不过,展开是由代价的。如果这个函数的代码指定很庞大,或者像递归函数这样的函数,对于这样的函数,如果在每次调用时都展开,那指令的数量将会非常多,**这样会使得可执行文件的大小变得很大。**所以,inline只是给编译器一个建议,如果编译器觉得这个函数不适合内联,那么它依然会像普通函数那样创建函数栈帧,在链接时进行符号决议和重定位等等操作。
• 🌰2.空指针nullptr
C的空指针NULL其实是一个宏,在传统的C头文件(srddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
🔥C++中NULL可能被定义为字面量0,或者C中被定义为无类型指针(void*)的常量。不论采用何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,**本想通过f(NULL)调用指针版本的f(int* ptr)函数,但是由于NULL被定义成0,调用了f(int x),**因此与程序的初衷相悖。f((void*)NULL);调用会报错。
🔥C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,**它可以转换成其他任意类型的指针类型。**使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式转换成指针类型,而不能被转换成整型。
cpp
#include <iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
f((void*)0);
f(nullptr);
return 0;
}
上面的代码中,f(0)毫无疑问结果是f(int x);而f(NULL),C++中NULL就是0,所以也是f(int x);而C语言中NULL是((void*)0),虽然是指针类型,但是是**void*泛型指针,而不是int*,**所以两者都不匹配,编译器会报错。**而f(nullptr)中的nullptr可以转换成其他任意类型的指针变量类型,且只能是指针类型,**所以它的结果是f(int * ptr)。
C语言和C++在void*能否隐式类型转换上存在差异:
cpp
#include <stdio.h>
int main()
{
void* p1 = NULL;
int* p2 = p1;
return 0;
}
C++是兼容C语言语法的,但是C++在语法上会比C语言严格,例如上面的代码,在C语言文件下是没有问题的,而C++不行。其原因是因为C语言的void*可以隐式类型转换,而C++不行。
• ✨SumUp结语
到这里本篇文章的内容就结束了,**本节初步带大家了解了C++这门语言的历史,也给大家介绍了C++的基础知识、语法。**这是C++的基础,希望大家能够认真学习。下一章节开始,我们会开始学习C++中的第一大关------**类和对象,**大家先打好基础,迎接接下来的挑战,希望大家继续捧场~