C++是什么:
用较为简单易懂的话来解释,就是在C的基础上改进的新语言,+的意思就是PLUS,因为是基于C语言的改进,所以引入了一些新的概念、语法,同时它也兼容C。
关键字
跟C语言一样,C++同样有一些平时常用的单词,例如int char之类的,这里我放一张表,记不住也没有关系,之后学了才会有更深的理解。
下面是表:
命名空间
这里要提到的一点是命名空间,相较于其他关键字,namespace出现的频率会高一些,在C++程序里的前几排,总有这么一行代码:
using namespace std;
要想理解这句话,就应该从什么是namespace说起,先来看一下官方的解释:
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化 ,以 避免命名冲突或名字 污染 , namespace 关键字的出现就是针对这种问题的。
![](https://i-blog.csdnimg.cn/blog_migrate/0b9cc2e0a3f7e867067032e6a804c868.png)
这种情况下,就可以用到namespace。
namespace N1
{
int a;
}
namespace N2
{
int a1 = 10;
}
namespace N3
{
int c;
namespace N2
{
int a1 = 10;
}
}
两种都是可以的,namespace的作用就是将花括号里面的内容单独开辟一块空间出来,这块空间是不会与主程序产生冲突。
函数、结构体...这些都是可以放在命名空间里面的。
命名空间是可以嵌套使用的,但如果命名空间的名字相同,比如说这块空间是N1,另一块空间也是N1,那最后处理的时候它们就会合并到一起去。
命名空间就像新开辟了一块空间,命名空间里面的内容都局限于这一块空间中
如果要调用这块命名空间的内容,那就有两种办法,第一,是指定一块空间让系统在那里面去找。
举个例子
这里就是很典型的例子,可以看到主程序里面并没有a1,但是我在它的左边加了两个双引号,并在双引号左边加了一个N2。
这行代码就是让程序在N2这块空间里面找一个叫a1的数字。
第二种办法是直接释放这块命名空间,第一种办法每次都只是去找其中一个元素,使用的次数一多,每次都加双引号和命名空间名是件很麻烦的事,所以为了解决这个问题可以一次将这里面的元素释放出来,这就需要在前面加一句
using namespace xxx;
这里又有两种用法,第一种是:
using N2::a1
![](https://i-blog.csdnimg.cn/blog_migrate/f887c4f21a7b2a997015d9fae1664edc.png)
如果是一次释放完全的话,那就应该在前面加上namespace,表明释放的是一块命名空间而不是单个元素。
那之前提到的:
using namespace std;
的意思就是将std里面的全部东西释放出来,经常使用的函数等...
C++输入&输出
就好像新学C语言是先在屏幕上打印一个“hello world”一样,C++也先从这里开始比较好。
先输出吧,C++的输出跟C的区别这里就能看出来,假设要打印一句:hello world
cout表示要输出东西,随后用两个左箭头,再用双引号将文本括起来,最后再用两个左箭头隔开,endl是换行的意思。
这里有几点要注意:
1. 使用 cout 标准输出 ( 控制台 ) 和 cin 标准输入 ( 键盘 ) 时,必须 包含 < iostream > 头文件 以及 std 标准命名空间。注意:早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带.h ;旧编译器 (vc 6.0) 中还支持 <iostream.h> 格式,后续编译器已不支持,因此 推荐 使用 <iostream>+std 的方式。2. 使用 C++ 输入输出更方便,不需增加数据格式控制,比如:整形 --%d ,字符 --%c
头文件肯定是要包含的,std命名空间要用记得释放。
这里要说的是第二点,C++输出的时候不要增加类型,编译器会自动识别,这里将输入也连起来:
创建三个变量,输入有要用cin,随后顺序输入,中间用向右的箭头连接,表示下一个输入的是它。
输入的时候跟刚刚一样,只不过中间的空格需要手打比较麻烦,不过这里可以看到没有提示类型它也可以直接输出,相较C舒服多了。
缺省参数
这里是跟C差异较大的地方,在C里面,函数声明有几个参数,那你传过去就要几个参数,但是在C++里面,就不是这样的。
先从概念说起:
缺省参数是 声明或定义函数时 为函数的 参数指定一个默认值 。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
举个例子:
这里就是调用了缺省参数,我参数里面接受类型里面定义了一个变量a,但是我传参数的时候缺没有传a的值过去,那C++就默认用函数参数里面定义的那个值,这里是 10。
但是如果我传来一个参数过去:
这里如果我传了一个2作为参数过去,那函数调用就会优先使用我传过去的参数。
函数重载
在C里面,函数名是不可以冲突的,但是在C++里面,就不会有这个问题,举个例子:
这里就是重载函数,它函数名一样,但是函数的参数不一样,根据我传过去的参数不同,系统会调用对应的函数。
然后,再来看一下对于重载的说明:
函数重载 : 是函数的一种特殊情况, C++ 允许在 同一作用域中 声明几个功能类似 的同名函数 ,这些同名函数的 形参列表 ( 参数个数 或 类型 或 顺序 ) 必须不同 ,常用来处理实现功能类似数据类型不同的问题。
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
这算吗?
它的函数名不同吗?
它的参数个数不同吗?
它的参数类型不一样吗?
但是它只有返回值不一样,所以它并不算重载参数。
重载参数的实现
这里就要从底层实现开始说,当程序开始运行的时候,它会经过几个阶段:
1、预处理:头文件展开、宏的替换、条件编译、去掉注释
这里会生成 .I文件
2、编译:检查语法,生成汇编代码
这里生成 .s 文件
3、汇编:将汇编代码转换为二进制的机器码
这里生成 .o文件
4、连接:开始找要调用的函数地址,然后将其连接在一起
这里生成 .out文件
那函数重载,C/C++就是在连接部分不一样,因为我还不会Linux,所以这里先说一下结论,之后再将详细过程写出来。
总之,C在连接过程中会很简单的将函数名对应起来,如果相同那它就会产生冲突
但是C++不是,它的函数名对应是这样的:_Z+函数名+参数首字母
假设是这样的一个函数:
C里面应该是两个fun函数,但是C++是这样的:_Z3i 和 _Z3d
因为函数名的参数不一样,它的函数名就不一样,所以在找的时候就不会有冲突问题。
引用
这个我个人认为是指针的改版,举个例子:
我现在要交换两个数,如果是指针,那应该这么写:
如果是用引用,那它应该是这样的
同样可以达成想要的效果,那再来看看引用的解释:
这里要注意两点,引用类型应该和引用实体是一个类型(但是如果不是一个类型,加一个const也是可以的)。
引用特性
1. 引用在 定义时必须初始化2. 一个变量可以有多个引用3. 引用一旦引用一个实体,再不能引用其他实体
这是可行的,但是如果引用没有初始化,那就会报错:
同时,还可以:
引用引用(套娃)
但是引用只有一个对象,不能再引用其他对象,这里就要注意下。
同时,这里还要注意一点:
引用只能将权限缩而不能放大:
举个例子:
在这里就是一个很明显的例子,const修饰的对象是不能被修改的,而你引用的权限是可以修改,所以就会报错。
如果要引用,那在引用的时候就应该也带上const
引用还可以作为返回值,作为返回值返回的是那一块空间的地址。
但是这种是错误的示范,因为函数返回的是n的别名(本质还是地址),但是n是一个局部变量,出了函数就会销毁,在销毁之后再去访问就会出现问题——越界。
再看这样一个代码,这里拿ret来接收了它的返回值,但是之后又调用了这个函数,在之后打印结果是?
本意是 3.但是引用返回的是c的地址,在1+2后又对这一块空间进行修改:
所以它就出错了。
如果再次看结果,这就是一个随机值了,因为这一块空间已经归还给了系统,再去看就被刷新了。
引用的效率问题
如果是这样的一个程序:
明明c的寿命周期已经到了,那为什么还能返回?
因为系统会将这个值暂存下来,可以理解为临时拷贝一份,那既然是临时拷贝,这效率就可以直说。
它没有引用块,假设这个数据很大,有一百万个数据,一千万个数据 ,那拷贝就要耗时间,而引用是直接返回地址,所以会更快。
相较于指针,引用和指针应该是一个级别。
引用和指针的区别
在概念上,指针是一块地址,引用是变量的别名,它们占用空间就有区别
但底层实际方面,引用跟指针实现方式很像,也是要占用一块空间的。
1. 引用 在定义时 必须初始化 ,指针没有要求2. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体3. 没有 NULL 引用 ,但有 NULL 指针4. 在 sizeof 中含义不同 : 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4 个字节 )5. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小6. 有多级指针,但是没有多级引用7. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理8. 引用比指针使用起来相对更安全
内联函数
当使用函数的时候,会有这么一种情况,就是:如果这个函数很短,但是需要用的地方很多,比方说实现一个A+B的函数,有没有什么办法可以解决这个问题?
当然,宏,那宏是怎么写的呢?
这里给你十秒,想一个宏实现A+B。
想出来了吗?
--------
因为宏直接替换,不好理解而且不方便查看过程,很容易出错,那这里就引入一个概念叫内联函数。
这里先看一下内联函数的定义:
以 inline 修饰 的函数叫做内联函数, 编译时 C++ 编译器会在 调用内联函数的地方展开 ,没有函数压栈的开销,内联函数提升程序运行的效率。
要理解这句话,要先理解什么是函数栈帧,每调用一个函数,都会在内存中开辟一块空间,然后压栈。
但如果是使用内联函数,那么程序运行就会直接在函数调用地方展开,不压栈,这样效率会更高。
但是如果这个函数很长,再用内联函数的话,每次调用都展开未免也有点太长了,所以编译器在执行的时候会先判断这个函数内容多不多,如果这个函数内容很多,它会不执行内联,如果很短,它会先替换。
那么多少代码算长的?
10行左右。
如果你想,所有函数前面都可以加一个inline,让编译器自己判断。
--------
内联函数的特性
1. inline 是一种 以空间换时间 的做法,省去调用函数额开销。所以 代码很长 或者有 循环 / 递归 的函数不适宜使用作为内联函数。2. inline 对于编译器而言只是一个建议 ,编译器会自动优化,如果定义为 inline 的函数体内有循环 / 递归等等,编译器优化时会忽略掉内联。3. inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
这里重点要了解第三条,如果分离,展开的时候就找不到函数本体,这里看一个例子:
在Code.h里面我声明了一个内联函数,但是我没有定义,而是在:
Code.h里面定义了这个函数,如果现在要用这个函数,会发生什么?
无法解析的外部符号,这里就可以直接理解为程序运行的时候找不到这个函数,但是我明明声明了这个函数,为什么还是找不到?
因为inline被展开之后它没有定义,只是一个声明,既然只有一个声明那它怎么可能调用呢?
这里要注意一下。
这里把宏的优缺点说一下:
优点:1. 增强代码的复用性。2. 提高性能。缺点:1. 不方便调试宏。(因为预编译阶段进行了替换)2. 导致代码可读性差,可维护性差,容易误用。3. 没有类型安全的检查 。
C++ 有哪些技术替代宏 ?1. 常量定义 换用 const2. 函数定义 换用内联函数
这里第一条有在《EffectiveC++》的条款里面提到:
EffectiveC++ 条款02:尽量以const,enum,inline替换#define
这里看图:
可以看到,maxa已经有值了,但是无法识别出MAX。
为了解决这种类似问题,就可以用const——无法修改的变量和enum枚举常量来代替。
auto关键字
当定义一个变量的时候,应该是这样定义的:
int i = 1;
float j = 1.1;
char k = 'A';
auto的作用就是,它会根据后面变量的数值来给类型:
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量 ,但遗憾的是一直没有人去使用它,大家可思考下为什么?C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型 指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得 。
如果用auto,是怎么样的?
可以看到基本是一样的。
但如果开始并未赋值,就会报错:
因为auto是根据后面的值来判断类型,如果不给值那它就无法判断。
如果用auto来定义指针,会发生什么?
可以看到,如果是auto + &,那auto就会默认是int*
如果是第二种,auto后面已经带*了,那auto还是int*。
如果是引用,那就应该是这样:
还要注意,如果一行定义多个变量,那这些变量都应该是一个类型:
同时,它不能作为函数参数:
还是那句,它是根据后面的值来给类型的,并且必须初始化。
同时,不能声明数组:
auto在for循环内部
以往,遍历数组是这样的
现在,可以用auto:
它会自己遍历每个数打印,那能不能直接修改呢?
可以看到,第一个遍历已经修改了里面的值,但是到了第二个遍历可以看到还是原来的值。
这是因为这里面的e是一份临时拷贝而不是对应的那个值,如果通过这个修改值,那有什么方法?
别名。
可以看下图:
这样就可以了。
指针空值nullptr
这个是后来的规定,大致意思:
C语言有以下两种办法指定空指针:
int*p = 0;
int* pp = NULL;
这两种都是制定为空指针,但是在C++(11)的标准里面,不算规范写法,是因为这两种写法的等价的:
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif#endif
这是网上找到的C头文件,可以看到这里是直接讲 NULL替换为 0,或者是void类型的指针。
这里有办法可以看看:
这是一个重载函数,但区别是一个是指针一个是整形,前面两个传参是 0 和NULL,看右边可以看出来它们调用的都是整形的函数,这里就可以看出来NULL跟0是一样的,而如果在传参时将NULL强转为int*,那它才会调用第二个函数。
那规范的空指针写法是什么样的?
如图,nullptr是规范的空指针写法,传过去的也是一个指针。
这里有几点注意:
nullptr是关键字,不需要头文件
在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
能用nullptr,就用nullptr,这样比较规范