前言
本篇博客讲解一下c++得入门基础
💓 个人主页:普通young man-CSDN博客
⏩ 文章专栏:C++_普通young man的博客-CSDN博客
⏩ 本人giee:普通小青年 (pu-tong-young-man) - Gitee.com
若有问题 评论区见📝
🎉欢迎大家点赞👍收藏⭐文章
目录
C++发展历史
C++是一种强大的多范式编程语言,它的历史可以追溯到1979年,由丹麦计算机科学家Bjarne Stroustrup在贝尔实验室开始研发。以下是C++发展的主要里程碑:
起源与初期阶段(1979-1985)
1979年:Bjarne Stroustrup开始基于C语言开发一种新的语言,最初称为“C with Classes”,目的是结合C语言的高效性和面向对象编程的特性。
1983年:语言正式更名为C++,名称由Rick Mascitti建议,灵感来源于C语言中的“++”运算符,象征着对C语言的增强。此时,C++已经成为一种独立的语言,不仅仅只是C语言的简单扩展。
1985年:首个正式版本的C++发布,包含了一些基本的面向对象特性,如类和继承。
标准化与C++98(1990s)
- 1998年:C++的第一个国际标准ISO/IEC 14882:1998发布,通常被称为C++98。这个标准引入了模板、命名空间、异常处理等特性,极大地增强了语言的功能。
技术进步与C++11(2011年)
- 2011年:C++11标准发布,这是一个重要的里程碑,引入了诸如智能指针、lambda表达式、自动类型推导、范围for循环和并发编程支持等现代特性,显著提高了C++的易用性和安全性。
进一步的现代化(C++14至C++20)
2014年:C++14标准发布,它对C++11进行了细化和补充,提供了更多的便利性和语言一致性。
2017年:C++17标准发布,引入了变量模板、文件系统库、并行算法等功能,继续推动C++向现代化方向发展。
2020年:C++20标准发布,它包含了概念(Concepts)、模块(Modules)、更大的类型安全性和更多实用工具,使C++更加现代化和易于使用。
当前与未来
C++20之后,C++23标准预计在2023年发布,继续增强语言特性和库,包括改进的概念支持、更好的错误处理机制、以及对模块系统的持续改进。
C++老登:Bjarne Stroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不 同的地⽅可能有差异)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作
这老登长这样:
c++版本
c++的相关参考文档
cppreference.com //官方文档
C++ 参考手册 - cppreference.com //官方文档汉化
c++基础
c++的第一个程序:
#include<iostream>//标准IO(输入/输出)流
using namespace std;//标准命名空间
int main() {
cout << "Hello C Plus Plus" << endl;
return 0;
}
ok现在我们已经掌握了c++的输出了
命名空间
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全 局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名 冲突或名字污染,namespace关键字的出现就是针对这种问题的。
举一个C语言的例子来对比
#include<stdio.h>
#include<stdlib.h>
int rand = 10;
int main() {
printf("%d\n",rand);
return 0;
}
为什么会报rand重定义的错误?
这个是#include<stdlib.h>中的,就是因为有名字重复,所以有时候你做项目的突然需要一个头文件的时候,突然报了个重定义错误,或许你会说我不定义全局变量就行了,但是这是避免不了的,你可以去看一下我的项目专栏
这种问题后来就被我们的c++老登给优化了,于是就出现了namespace(命名空间),那我用命名空间写一下:
#include<iostream>
#include<stdlib.h>
namespace YM{
int rand = 10;
}
int main() {
printf("%d\n", YM::rand);
}
可以看到我这里自己定义了一个命名空间,为什么这里就不会有影响了嘞?
定义
命名空间的声明语法非常简单,通常形式如下:
namespace 命名 {
// 命名空间内的定义
}
除了函数局部域、全局域、命名空间域和类域之外,C++中还有文件作用域、命名空间作用域、类作用域等,它们共同构成了C++的命名系统。每个域不仅影响着编译器如何解析标识符,还可能影响到变量的生命周期和可见性,但需要注意的是,命名空间域和类域本身并不改变其内部变量的生命周期。
这里提到的域,我相信大家应该都懂,作用域不一样,所以就不会一样,YM::rand 就是访问YM这个命名空间域里面的rand
::
操作符被称为作用域解析操作符
全局作用域解析:当你在一个局部作用域(如函数体内)中想要明确引用全局作用域的某个标识符(如全局变量、函数等)时,你可以使用::
来指明你要访问的是全局作用域中的那个标识符,而不是当前局部作用域中可能存在的同名标识符。例如:
#include<iostream>
using namespace std;
int x = 20;
int main() {
int x = 10;
cout << ::x << endl;
return 0;
}
这里加了域操作符,但是前面啥也不加,就是访问的全局域
命名空间的嵌套定义:
#include<iostream>
namespace YM {
int x = 10;
namespace xy {
int x = 20;
}
}
namespace tmp {
namespace x {
int rand = 20;
int sum(int x, int y) {
return x + y;
}
}
namespace y {
int rand = 30;
int sum(int x, int y) {
return x * y;
}
}
}
using namespace std;
int main() {
cout << YM::x << endl;
cout << YM::xy::x << endl;
cout << tmp::x::rand << endl;
cout << tmp::y::rand << endl;
cout << tmp::x::sum(3, 2) << endl;
cout << tmp::y::sum(3, 2) << endl;
return 0;
}
大家可以把这个代码拷贝过去自己感受一下
多文件在一个命名空间
我们可以看到这里我将栈和队列都放在一个命名空间里面
从这里你是不是有一种封装数据结构的感觉,这里就可以证明我们如果在公司里面将我们写的程序弄到一个namespace1空间中然后上传到一个namespace2里,然后我的同事用同样的方法也可以进行上传到namespace2,因为域之间互不干扰,这样就会非常爽!!!
命名空间的展开
这个是标准命名空间的展开
我们也可以自己定义
不展开
展开
你可以看出来它展开和不展开的区别:
可读性和清晰度:不展开命名空间意味着你需要显式地指定元素的来源,这可以增加代码的可读性和清晰度,尤其是在多个命名空间中使用相同名称的情况下。
避免命名冲突:展开命名空间可能导致与现有命名冲突,特别是与标准库中的命名冲突。例如,
std
命名空间包含了C++标准库的大部分功能,如果使用using namespace std;
,你可能会意外地覆盖其他命名空间中的同名元素。代码维护:不展开命名空间可以使代码更易于维护,因为你总是知道一个特定标识符的确切来源。
性能:虽然在现代编译器中性能差异可能微乎其微,但在理论上,使用作用域解析操作符可能会稍微增加编译时间,因为它需要更多的解析步骤。
C++的输入输出
输入输出流(I/O Stream)
概述:
<iostream>
是标准的输入输出流库,定义了标准的输入、输出对象,简化了数据的输入输出处理。
std::cin
:istream
类的对象,主要面向窄字符(类型为char
)的标准输入流,用于从标准输入设备(如键盘)读取数据。
std::cout
:ostream
类的对象,主要面向窄字符的标准输出流,用于向标准输出设备(如显示器)发送数据。
std::endl
:是一个控制字符,用于流插入输出时,相当于插入一个换行字符并刷新缓冲区,确保数据立即显示。运算符:
>>
和<<
分别是流提取和流插入运算符,在 C++ 中用于输入输出,不同于 C 语言中的位运算左移和右移。优势:C++ 的输入输出操作更加便捷,自动识别变量类型,无需手动指定格式,支持自定义类型对象的输入输出,体现了面向对象的特性。
面向对象特性:IO流涉及类和对象、运算符重载、继承等面向对象的概念,这些概念将在后续学习中深入探讨。
命名空间:
std
是 C++ 标准库所在的命名空间,cout
、cin
、endl
等都属于此命名空间。在日常练习中,常用using namespace std;
来简化调用,但在实际项目中,为了避免命名冲突,建议显式使用std::
前缀。兼容性注意:尽管未明确包含
<cstdio>
(提供printf
和scanf
),在某些编译器环境下(如 VS 系列),它们可能被隐式包含,但在其他编译器上可能需要显式包含才能避免错误。注意:
对cout的解释,这里里面的函数重载我待会解释
流插入运算符
<<
:当你使用cout
输出数据时,实际上是将数据通过流插入运算符<<
插入到cout
流中。例如,cout << 123;
就是将整数123
插入到cout
流中。类型转换:
cout
在背后做了大量的工作,将你提供的数据类型转换成文本格式。例如,如果你有一个整数123
,cout
会将其转换成字符串"123"
,然后再输出到屏幕上。函数重载:
cout
的这种能力是通过函数重载实现的。cout
实际上是一个ostream
类型的对象,ostream
类为各种数据类型重载了<<
运算符。这意味着<<
运算符根据左右两边的数据类型,会选择正确的重载版本来执行正确的转换和输出操作。缓冲与刷新:
cout
输出的数据会被暂时存储在一个缓冲区中,直到缓冲区满或者程序显式要求刷新(如使用endl
)。这有助于提高性能,因为一次性写入大量数据比频繁写入单个小数据块效率更高。
这边我就不和大家说那些没用的废话直接上代码,我在代码下面放个解析
#include<iostream>
using namespace std;
int main() {
//一
int a = 10;
cin >> a;
cout << a << endl;
//二
int b, c, d;
cin >> b >> c ;
cout << b << endl << c << endl;
cout << b << " " << c << ' ' << endl;
}
解析
#include<iostream>
这行代码包含了标准输入输出库(iostream),使得程序可以使用输入输出相关的功能,如cin和cout。
1using namespace std;
这行代码告诉编译器使用std命名空间,这样我们就可以直接使用cout和cin等函数,而不需要每次都写std::cout或std::cin。
1 int a = 10;
声明了一个整型变量a,并初始化为10。
1 cin >> a;
从标准输入读取一个整数并将其存储在变量a中。这会覆盖之前给a的初始值10。
1 cout << a << endl;
输出变量a的值到标准输出(通常是屏幕),并在后面添加一个换行符。
1 int b, c, d;
声明了三个整型变量b、c和d。
1 cin >> b >> c ;
从标准输入读取两个整数,分别存储在变量b和c中。
1 cout << b << endl << c << endl;
输出变量b和c的值,每个值后都加上一个换行符,使得b和c的值在不同的行显示。
1 cout << b << " " << c << ' ' << endl;
输出变量b和c的值,中间用空格分隔,然后在最后添加一个换行符。
缺省函数
定义
- 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参 则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把 缺省参数也叫默认参数)
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左 依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省 值。
代码举例
这里我们缺省了1这个参数(半缺省)
这边我还可以直接缺省两个参数(全缺省)
你不要觉得这个没有用,比如你在开空间扩容的时候,你就可以给一个默认值,就会方便很多,如果突然需要一个很大的空间,你直接传个值过去就覆盖掉默认值
重载函数
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者 类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同 名函数的。
这个在C语言就是没有的,你们会发现有了这个重载函数会非常爽,在C语言中,如果我们换了一个类型,我们就的重新命一个函数,不支持一个函数名支持多个种类型
在定义重载函数,一定保持参数顺序相同,不然就会报错,这也是老登规定的,没有为什么
返回值不同不能作为重载条件,因为调⽤时也⽆法区分
我们来看一个纯在歧义的重载函数,这种以后千万别写,我感觉有一点画蛇添足
不传参,会报错,因为第一个无参,第二个是缺省函数
当我给他传上参数他又满血复活了,哈哈哈
引用
引用的概念和意义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋 ⻛";林冲,外号豹⼦头
类型& 引用别名 = 引用对象
就等于你给一个地址的地方去了a,b,c,d几个别名,都可访问这个地址
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的>,这⾥引⽤也和取 地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。(吐槽⼀下,这个问题其实挺坑的,个 ⼈觉得⽤更多符号反⽽更好,不容易混淆)
其实简单来说就是取了个别名
我们打印一下他们的地址看一下是不是一样
其实这里已经说明了他们底层是指针,大家可能觉得这样写会有点怪,哈哈,其实这个在函数传参的时候,会非常方便
引用的特性
- 引用在定义时必须初始化
- ⼀个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
这里有一个案例,很多同学喜欢把这个认为是在改变a
这里的c只有一个赋值作用,我们可以看出地址是不一样的,如果这里认为c是在改变a的话说明你C语言指针需要复习一下,哈哈哈,加油少年
引用的使用
- 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被 引⽤对象。
- 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
- 引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节 中会继续深⼊讲解。
- 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他 语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向, Java的引⽤可以改变指向。
- ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开 复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
接下来我用代码解释这个引用的应用
看吧,我这个很形象了吧,用引用的话,我们就可以不用再传参数的时候写一个&地址符,这样也好理解一点,我估计有可能老登也觉得指针有一点太复杂了
这种千万不要觉得奇怪,他是一个LTNode*的指针传参,自然也要是LTNode*的别名来接收
const引用
定义:
- 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访 问权限在引⽤过程中可以缩⼩,但是不能放⼤。
- 不需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场 景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对 象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥ 就触发了权限放⼤,必须要⽤常引⽤才可以。
- 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。
被const引用之后,权限就变小了,只能读不能写
这里是把我们的const的权限放大,所以报错,注意看我上面的定义
如果这样写,就不会放大权限
你们看这个代码,其实报错的原因很简单,就是他们都是有常性的,a*3是不是需要临时对象,double转换也需要临时对象,所以这里报错的很明显,这个ra和rb引用的就是临时对象
如果我加上const修饰的话,就不会报错了,而且你会发现,这样的话我们函数传参数啥类型都可以传,如果在配上函数重载,岂不是一个函数啥都传,啥都能做,真的爽!!!
简单说C语言const就是修饰常量,c++中const应用要考虑常性和权限(只读)的问题
指针和引用的关系
咋说他们两犹如卧龙凤雏,难兄难弟
在C++中,引用(reference)和指针(pointer)都是用于间接访问另一个对象的方式,但它们之间存在一些重要的区别。以下是引用和指针的一些关键差异点:
内存分配:
- 引用并不占用额外的内存空间,它只是给已存在的变量一个别名。
- 指针是一个变量,它存储的是另一个变量的地址,因此它会占用内存空间。
初始化要求:
- 引用在创建时必须初始化,并且一旦初始化,就不能改变它引用的对象。
- 指针可以延迟初始化,也可以重新指向不同的对象。
解引用操作:
- 引用不需要显式地进行解引用操作就可以访问其绑定的对象。
- 使用指针访问对象时,通常需要使用解引用运算符(*)来获取指向的数据。
sizeof
操作:
sizeof
作用于引用时,返回的是引用所引用对象的类型大小。sizeof
作用于指针时,返回的是指针本身的大小,这通常是平台相关的(如32位系统上是4字节,64位系统上是8字节)。安全性:
- 引用提供了一种更安全的方式来处理数据,因为它们不能被设置为NULL,也不能重新指向另一个对象。
- 指针可以被设置为NULL,可以重新指向,这也增加了出错的可能性,比如空指针解引用或野指针问题。
总的来说,引用和指针在功能上有重叠的地方,但在使用场景和语义上有所不同。引用通常用于函数参数传递、作为类成员等场景,而指针则在需要动态内存管理、函数指针或者需要频繁改变指向的情况下使用。
inline
定义
使用inline
关键字定义的函数称为内联函数,其主要设计目的是为了减少函数调用的开销,从而在某些情况下提高程序运行效率。当一个函数被声明为内联时,编译器在遇到该函数调用时,可能会选择将函数体直接嵌入到调用点,这一过程称为内联展开。内联展开避免了传统的函数调用机制,如保存和恢复CPU寄存器状态、维护堆栈以及控制流转移,这些操作在频繁调用小函数时可能成为性能瓶颈。
然而,内联展开并非总是有益的。如果函数体过大,内联展开可能会导致目标代码膨胀,进而增加程序的加载时间和内存占用,甚至可能降低缓存效率,从而影响整体性能。此外,内联展开可能会增加编译时间。
以下是一些与内联函数相关的关键点:
inline
关键字向编译器发出一个建议,表明程序员希望该函数被内联。但这并不是一个强制性的命令,编译器可以选择忽略这一建议,特别是在函数体较大或在递归函数中。- 内联函数最适合那些频繁调用且代码量较少的函数,这样的函数从内联中获益最大。
- 不同的编译器可能有不同的内联策略,C++标准并未明确规定何时应该内联一个函数,这留给编译器制造商自行决定。
- 由于内联函数的代码直接嵌入到调用位置,因此它们不会建立独立的栈帧,这有助于减少函数调用的开销。
- 内联函数的另一个常见用途是在模板和类的成员函数中,尤其是在需要高效率执行的代码段中。
总之,内联函数是一种优化手段,用于减少函数调用的开销,但它是否真正提高性能取决于函数的特性、编译器的选择以及具体的运行环境。
内联函数的写法:
这个内联函数省去了函数调用,栈帧创建和销毁,避免了寄存器的使用
我用汇编代码给大家解释一下吧
call
指令用于调用函数,如call swap (07FF6F91C104Bh)
。
ret
指令用于从函数返回。
这里我们需要设置编译器,才能让编译器听从我们的建议
调好了之后,我们在进行反汇编
发现call指令不见了,就说明内联成功,其实你会发现这个内联函数,和C语言的宏有点像,其实就是改进的,因为宏定义函数非常的不方便容易出错,所以老登就发明了这个
nullptr
nullptr是c++中的空,为什么c++不用NULL?
这是因为NULL的定义是这样的
在C++中,NULL
的使用和含义在C++11之前和之后有着显著的不同。在C++11之前,NULL
可能被定义为以下几种形式之一:
字面值0:这是在C语言中常见的定义方式,
#define NULL 0
。在这种情况下,NULL
可以被当作整数0使用,这可能导致一些混淆,尤其是在模板和重载函数中,当0被传入时,它可能被解释为整数值而非空指针。
NULL
作为void*
类型:在C中,NULL
也可能被定义为void*
类型的指针,#define NULL ((void*)0)
。这种定义方式明确指出NULL
是一个指针,但仍然存在类型转换的问题。在C++11中,为了更好地解决这些问题,引入了新的关键字
nullptr
。nullptr
具有以下特性:
类型安全:
nullptr
是一个特殊的字面量,它只能够被隐式地转换为任何类型的指针,包括函数指针和成员指针。这意味着你不能意外地将nullptr
当作整数使用,提高了代码的安全性和清晰度。更少的类型转换问题:当你在函数调用中使用
nullptr
时,编译器会自动将其转换为所需的指针类型,而不需要显式的类型转换,如(void*)NULL
。与
NULL
的区别:在C++11中,nullptr
不同于NULL
。尽管在C++11中,NULL
仍然可用,但它被定义为nullptr
的宏。然而,使用nullptr
更加清晰和推荐,因为它强调了其类型安全的性质。
综上所述,使用 nullptr
能够避免因 NULL
被定义为整数0而引起的意外类型转换和调用错误,提高了代码的可读性和安全性。例如,如果你试图调用接受指针的函数,但传递了一个整数值,使用 nullptr
可以防止这种情况发生,因为 nullptr
不能被隐式转换为非指针类型。
你会发现他是一个0,一个整形,你可能觉得这没什么,但是你看我给你个案例:
看到了吗?这是一个重载函数,为什么都调用的是fun1,这就是弊端,按理说他是个NULL为什么一个整形接收了它。
欧克这节就结束了