C++ 入门知识前提

各位看官,我所使用的编译器是 vs2019

前言

关于这篇文章主要是想 用 C++ 解决一些在 C 语言上的缺陷

文件后缀

  • C :.c 文件后缀
  • C++ :.cpp 文件后缀,也就是 cplusplus

在 vs2019 里这个区别体现在:使用 .c 时会调用 C 的编译器;使用 .cpp 时会调用 C++ 的编译器

兼容

对于 C++ 来说,我们的学习应该在 C 的基础之上,所以有很多 C 的玩法在 C++ 里依然兼容!
比如:现在使用 C 语法写一个打印 Hello World! 的代码,但将文件后缀标为 .cpp 文件类型,会发现依然可以运行并兼容 C 的玩法!

 C 的玩法
可惜我们要写的是 C++ 代码,看下面 C++ 怎么做:

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello World!" << endl;
	return 0;
}

 C++ 的玩法

可以看到除了几个可能认识的关键字之外,其他的都是生面孔,那么接下来就用此程序一脚踢开 C++ 的大门!

第一个程序

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello World!" << endl;
	return 0;
}

如上程序的运行结果大家也已经看到,看到第一行是 #include <iostream> ,这是 C++ 的头文件,属于 C++ 的流,在这里并不是重点,而第二行的 using namespace std; 才是重点

命名空间 和 域作用限定符 ::

认识命名空间 和 域作用限定符 ::

先来看看 C 语法的一个不恰当的地方

在这里插入图片描述

C 语言里 不可以定义一个已经被定义了的变量名,上图很显然 rand<stdlib.h> 里的一个全局库函数,被重复定义,命名冲突

但也有一定的同名情况存在,在不同的域里面可以定义同名变量,那 C 就可以出现下面的情况:

在这里插入图片描述
可以看到第一个 x 是局部域的,第二个 x 才是全局的,域的划分可以让我们使用同名变量,在 x 的前面添加 :: ,此为 域作用限定符 ,在 :: 的左边什么都没给的情况下就是默认全局域的意思,所以第二个 x 就是全局等于 10 的 x ,就此区分开全局与局部

那么在 C++ 里常见有以下几种域:

  • 全局域:要指定访问全局域 :: 左边就是空的
  • 局部域:一般不需要指定,就是默认访问的 (局部优先),而函数不能代表域!!!
  • 命名空间域:接下来会讲
  • 类域:这个在后面会讲解

咱可以说避免使用与库函数同名的变量,但在现实工程里使用第三方程序员代码怎么可能不出现 命名冲突 的情况;所以 C++ 使用 命名空间 来解决此问题

在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的

namespace 看字面意思就知道这是 命名空间(名字空间)此关键字定义的是域 ,后面添加域名,看如下:

#include <stdio.h>
namespace test1
{
	int x = 10;
}
namespace test2
{
	int x = 20;
}
int main()
{
	return 0;
}

看上述代码,这就将两个 xmain 函数的外面封装了起来,但是先要理解下面的问题:

  1. 命名空间就是在全局域里开荒,将被开荒出来的范围进行所属,变为 指定域
  2. 两个 x 还是全局变量,只是被封装到命名空间的域里面,做了个名字的隔离,并且两个 x 不会冲突,所以命名空间封装的还是全局的东西
  3. 和全局域局部域要区分开,这哥俩对自己域内的变量既会影响生命周期,又会影响访问;而命名空间域内变量只能定义在全局,不会影响生命周期,只会影响访问,是对冲突变量进行隔离
  4. 编译器的 默认 搜索变量原则: 默认搜索变量原则是在 不指定域 内,也就是 当前局部域 --> 全局域 ,如果在这两个域内均搜索不到,就会报错,因为命名空间虽然在全局域里,但他俩并不是同一个域;所以你又该如何访问上述 test1test2 里的两个 x 呢?

域作用限定符 :: 就有用处了,看下图:

在这里插入图片描述

这样做就变相解决了编译器的变量搜索问题,相当于 去指定域内 搜索变量,所以对上述 rand 也就有了解决之法:
在这里插入图片描述

上图可以看到这似乎就完美解决了命名冲突的问题,由于库里的 rand 是库函数,也就是函数指针,所以咱打印出它的地址即可

关于命名空间还有几点需要注意:

  1. 命名空间的空间名可以重复,编译器会将重复的空间名合并为同一个域,此时域内不可有重名哦
  2. 命名空间也可以定义结构,函数等等,因为这些玩意均有可能会冲突
  3. 命名空间可以套娃,也就是命名空间里嵌套命名空间,只不过访问的时候也需要不断嵌套,例如: Bit::z111::Push(); 这里 Bit 是最外层的命名空间, z111 是第二层命名空间
  4. 在指定命名空间内的 结构体 时,需要注意将 域作用限定符 :: 放在结构体名称前而不是 struct 关键字前

在这里插入图片描述

展开命名空间

咱们看回第一个程序:

#include <iostream>
using namespace std;
int main()
{
	cout << "Hello World!" << endl;
	return 0;
}

using namespace std; 就是将名为 std 的命名空间域在全局域内展开,理解为将 std 命名空间的 访问权限打开,在全局范围内使用此域内的变量函数等等,比如 coutendl<<>> 等等
std 又是什么呢?std 就是 C++ 内所有的库命名空间,通俗点讲,就是要想使用 C++ 库里的东西,就要展开或指定 std 命名空间,所以也可以像下面这样写:

在这里插入图片描述

如果将 std 全展开势必会出现新的命名冲突情况,可是将库内变量函数等一次次指定又显得特别臃肿繁琐,所以咱可以像下面这样将使用频率高的进行展开即可:

#include <iostream>
using std::cout;
using std::endl;
int main()
{
	cout << "Hello World!" << endl;
	return 0;
}

C++ 的输入输出

针对上面的第一个程序而言,我们发现有 coutendl 之类的字眼,那他们是什么呢?其实从代码运行结果上看 cout << "Hello World!" << endl; 的作用就类似于 printf("Hello World!\n"); 一样将 Hello World! 输出在屏幕上并换行,以现在的知识储备是无法讲清 coutcin<<>> 之类的标识符的,所以目前就着其作用可以简单聊一聊

  1. 使用 cout 标准输出对象(控制台)和 cin 标准输入对象(键盘)时,必须包含 <iostream> 头文件
    以及按命名空间使用方法使用 std
  2. coutcin 是全局的流对象, endl 是特殊的 C++ 符号,表示 换行输出,他们都包含在包含 <iostream> 头文件中
  3. <<cout 配合的时候是 流插入 运算符,>>cin 配合的时候是 流提取 运算符。
  4. 使用 C++ 输入输出更方便,不需要像 printf/scanf 输出输入时那样,需要手动控制格式,C++ 的输入输出可以自动识别变量类型
  5. 实际上 coutcin 分别是 ostreamistream 类型的对象,>><< 也涉及运算符重载等知识

缺省参数

有了上面的认知,相信大家在一定程度上或多或少地感知到 C++ 的不同之处,似乎比 C 语言更加严谨,也可以说是填补了 C 语言的缺点,更加好用,缺省参数 就是鲜明的案例

缺省参数,也可以叫做 默认参数,是声明或定义函数时为函数的参数指定一个缺省值,在调用该函数时,如果 没有指定实参则采用该形参的缺省值 ,否则使用指定的实参

注意:

  1. 缺省值必须是 常量 或者 全局变量
  2. C语言不支持(编译器不支持)
#include <iostream>
using namespace std;
void fun(int m = 10)
{
	cout << m << endl;
}
int main()
{
	fun();
	fun(20);
	return 0;
}

在这里插入图片描述

相信看到这里应该发现了一丝丝端倪,相比于 C 来说,当 没有参数传递时 就会采用类似 m 后的值 10 ,这样使用起来也会更加便利,在以后学习更多之后会感觉更加深刻

当然可以不止缺省一个参数,也可以全缺省或者半缺省

全缺省

#include <iostream>
using namespace std;
void fun(int a = 10, int b = 20, int c = 30)
{
	cout << a << " " << b << " " << c << endl;
}
int main()
{
	fun();
	fun(0);
	fun(0, 1);
	fun(0, 1, 2);
	return 0;
}

在这里插入图片描述

需要注意以下几点:

  1. 如上代码可以看到全缺省就是 将此函数的所有的形参 都给上缺省值
  2. 在我们调用的时候发现,传实参的顺序是不能变的,只能从左往右依次传参
  3. 由于是 全缺省 实现,所以可以只传一部分实参;但顺序依旧是从左往右依次传参!也就是说如果只传一个实参就是传给了左边第一个形参,传两个实参就分别对应左边的两个形参,如上图代码结果,以此类推,所以是不能跳跃着传参的

半缺省

半缺省 就是对于函数参数 只给出部分缺省值 ,半缺省参数 必须从右往左依次来给出 ,不能间隔着给,如下图:

// 正确示范
#include <iostream>
using namespace std;
void fun1(int a = 10, int b = 20, int c = 30)
{
	cout << a << " " << b << " " << c << endl;
}
void fun2(int a, int b = 20, int c = 30)
{
	cout << a << " " << b << " " << c << endl;
}
void fun3(int a, int b, int c = 30)
{
	cout << a << " " << b << " " << c << endl;
}
int main()
{
	fun1();
	fun1(10, 11, 12);
	cout << endl;

	fun2(10);
	fun2(20, 21, 22);
	cout << endl;

	fun3(30, 31);
	fun3(30, 31, 32);
	cout << endl;
	return 0;
}

// 以下为错误示范
void fun1(int a = 10, int b, int c = 30)
{
	cout << a << " " << b << " " << c << endl;
}
void fun1(int a = 10, int b = 20, int c)
{
	cout << a << " " << b << " " << c << endl;
}

在这里插入图片描述

缺省值声明定义的位置

请记住缺省参数不能在函数声明和定义中同时出现;假如将声明定义进行分离,声明在 .h 头文件,定义在 .cpp 源文件,那么此时 缺省值应该放在声明处

我们知道程序想要运行分为 预处理编译汇编链接运行 阶段

  1. 预处理 :这个阶段的作用是 展开头文件宏替换条件编译去掉注释 等等,所以 .h 头文件并不参与编译阶段
  2. 编译 :检查语法,语法无误再进一步生成汇编代码
  3. 汇编 :把汇编代码转成二进制机器码
  4. 链接 :将 .cpp 文件合并到一起

从上面介绍看到,将函数的声明与定义分离,那么声明参与 编译,到 链接 才 call 这个函数的定义地址调用函数,而传参是编译层面的检查,如果没有缺省值且无实参传递就会出现编译错误,所以需要将缺省值放在声明处

函数重载

首先需要明白 C 语言是不允许同名函数存在的,而 C++ 却可以!!!这就是 函数重载,C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表( 参数个数类型类型顺序 )不同,常用来处理实现 功能类似数据类型不同 的问题

认识函数重载

我们知道任何一个可以被允许的事情都是需要条件的,所以这里先说明 函数重载 的条件

  • 函数名相同
  • 参数不同(参数类型 不同、参数个数 不同、参数顺序 不同)
  • 返回值不参与函数重载条件判定

先来看看例子:

#include <iostream>
using namespace std;
int Add(int m, int n)
{
	return m + n;
}
double Add(double m, double n)
{
	return m + n;
}
int main()
{
	cout << Add(1, 2) << endl;
	cout << Add(1.1, 2.2) << endl;
	return 0;
}

在这里插入图片描述

如上图,我们看到好像连参数是自动匹配函数的,整形参数 运算对应第一个 Add浮点型参数 运算对应第二个 Add ,换句话来说只管用,只要是参数都对的上就好,编译器会自动帮助匹配

函数名修饰规则

C 语言并不支持函数重载,但 C++ 是如何支持的呢?

在函数声明与定义分离的场景下,如果只有声明没有定义,那么在 链接 的时候 一定会用 函数名 去找寻函数的地址 call 过来调用,如果函数名相同势必会造成冲突,所以 C 语言是区分不开的

函数名修饰规则名字中引入 参数类型,使得函数在链接时名字不相同;虽然都是函数名修饰规则,但并非一套规则,各个编译器实现方式并不一样

在这里插入图片描述
比如说 在 Linux 的 g++ 下 编译器规则:
首先在函数名前加上 _Z ,接着要在 _Z 后面添加 这个函数名的长度
在函数名后则需要添加类型首字母,得保证顺序不可变

所以上图:

// 第一个
_Z1fic()
// 第二个
_Z1fci()

所以在 Linux 的 g++ 下,主要用函数名后面的类型区分重载,那为什么要在 _Z 的后面添加函数名长度?当然是因为快捷,不同函数的函数名长度应该是不同的,那编译器应该怎么知道什么地方是函数名,什么地方是类型后缀呢,例如下面:

void fun(double a, int b, int c);  // _Z3fundii
void fund(int a, int b);  // _Z4fundii

至于 vs 下的 函数名修饰规则 太复杂,比如我现在只写函数声明却无定义:

在这里插入图片描述

可以看到,上图红色方框括起来内容,但后面有个这样的字符串

?f@@YAXHD@Z
?f@@YAXDH@Z

这是什么呢?仔细看会发现他俩并不相同,只是最后一个 @ 前的 HD 换了位置,这就是 vs 下的 函数名修饰规则 ,晦涩难懂,不再赘述;上述代码中由于函数只有声明没有定义,所以靠这两个修饰的函数名是找不出函数地址的,也就出现 链接 错误

引用

认识引用

什么是引用

什么是引用呢?它的作用就是 取别名 !用在数据类型的后面,写作 &注意 不是 取地址和按位与 的意思!!!& 仍然有取地址和按位与的作用,注意使用时的区别即可区分

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为 引用变量 开辟内存空间,它和它引用的变量共用同一块内存空间

可以先不用刻意理解 引用 的字面意思,举个例子:对于一个变量 a ,总是要为其分配空间,当我们使用 a 这个 变量名字 时即可以找到这块空间并使用空间里的值,而 引用 操作的还是 a 这块空间,只是 引用 喜欢叫这块空间 b 罢了,就像是给这块空间取了个别名 b ;类似于有个人的名字叫张三,而别人也喜欢叫他狗剩

注意共用一块空间的利害,下面进行引用的操作:

#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	int& b = a;
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

在这里插入图片描述

在这里可以看到, ab 就是 同一块空间 ,地址均为 00EFFA6C ,也就是说修改任何一方,另一方也一定会发生改变,且 ab 的值一定是一样的,就是因为 在同一块空间 的缘故:

#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	int& b = a;
	++a;
	cout << a << " " << b << endl;
	++b;
	cout << a << " " << b << endl;
	return 0;
}

在这里插入图片描述

有趣的是:如果你有需要,别名可以 在同一块空间 一直 无限取 ;且 可以给别名取别名 ,别绕进去咯,上面代码里 ba 的别名,同样也可以给 b 取别名叫做 c

引用使用原则

  1. 引用必须初始化! 就是说你在定义一个引用变量时必须是某个变量的别名,不可以像这样 int& b; 这是大忌!就像现实中你能说出不依附于人或物的外号吗?
  2. 引用不可改变指向 ,也就是说你初始化为 a 的别名后就不可以再成为其他变量的别名!这样 a 会生气的

初步感受引用价值

总是感觉这个 引用 取别名看起来没什么用啊!其实不然

做输出型参数

什么叫做输出型参数?就是需要函数形参影响实参的情况,要在函数里改变外面实参的值,典型的就是大家做牛客力扣会出现的 int* returnSize 参数,需要在函数内返回你修改 nums 后的长度,就是为了在外面可以通过参数就可以拿到值

看一个活生生的例子:变量交换

// 指针实现
void Swap(int* left, int* right)
{
	int temp = *left;
	*left = *right;
	*right = temp;
}
// 引用实现
void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}

在这里插入图片描述

以前的做法中,利用函数交换变量的值 肯定需要 传址传参 ,借助指针的威力;因为如果直接 传值传参 ,而函数形参是实参的一份临时拷贝,函数形参和实参根本就不是同一块空间,函数形参改变不影响实参,将无法完成交换,被迫传递实参地址交换;而现在我们借助 引用 就可以 避免使用指针,因为 函数形参和实参就是同一块空间,此时函数形参就是实参而不是拷贝,改变函数形参就是改变实参!!!

可以看到此时的 引用指针 更加方便 ,我们不用 指针 就可以在函数内修改函数外的值;除此之外,不用再额外开辟函数形参的空间,可以直接拿函数外的值用,所以 引用代替指针效率更高,调用下面的 TestRefAndValue() 就能知道差异

#include <time.h>
struct A 
{
	int a[10000];
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

做返回值

先问大家一个问题,看下列代码:

#include <iostream>
using namespace std;
int func()
{
	int a = 10;
	return a;
}
int main()
{
	int ret = func();
	return 0;
}

请问上述代码运行后 是否会将 a 赋值给 ret ,换句话说函数 func() 的返回值是不是 a 呢? 不是!!!

函数的建立与销毁 来看,当需要调用一个函数是,操作系统会在栈上开辟空间为其提供资源;当一个函数需要销毁时,势必意味着空间将还给操作系统,那么函数栈空间里的变量所用空间也会一律还给操作系统,那么上述 func() 函数里变量 a 在函数销毁后也将不复存在,又如何可以被赋值给 ret 呢?

ret 是被谁赋值给来的呢?在 func() 函数彻底销毁之前,将会把 a 里的值交给 寄存器 ,由 寄存器 赋值给 ret ,那么这样的效率和前面一样并不高,可能大家就要说了,咱也加个引用 & 呗,那咱就来分析下图表达的什么意思:

在这里插入图片描述

可以看到咱好像返回了个临时变量 a ,那是什么?我们知道随着函数的销毁变量 a 也被销毁,可销毁到底是什么意思?就是将这块空间的使用权归还操作系统,至于里面的值还有没有,咱就不知道了,可能有也可能没有,所以返回的是个随机值;因为咱引用变量 a 直接返回就相当于你使用指针指向一块不属于你管理的空间,出现类似野指针的情况 ,所以在这里不能使用引用返回,否则将出现不可控的 BUG !!!

那引用返回这么危险,为什么还要使用引用返回?显然有些变量不会随函数销毁而销毁,反而会一直保留,那就是静态变量

#include <iostream>
using namespace std;
int& func()
{
	static int a = 10;
	return a;
}
int main()
{
	int ret = func();
	return 0;
}

所以上述代码就没有任何问题,因为静态变量 a 不会随 func() 的销毁而销毁!那么可以使用 引用返回 的场景就是 全局变量、静态变量、堆上空间 等等,也就可以使效率更高

引用可以完全替代指针吗?

看到 引用 带来的价值,又鉴于 指针 似乎没那么好用,不由得想问: 引用 可以替代 指针 吗?当然不能!

C++ 的引用是对使用指针比较复杂的场景进行一些替换,让代码更加简单易懂,但这并不能完全代替指针,因为 指针可以改变指向,而引用不可以!!!,举个很简单的例子:现在要你用 引用 来实现 链表中间元素的插入与删除 你就做不到

题外话:在 Java 和 Python 这样的语言里有指针吗?没有的!所以它们语言实现链表就是通过引用实现!但它们的引用可以改变指向,和我们 C++ 不一样

引用和指针的区别

既然引用不能完全替代指针,那它们之间的主要区别是什么呢?

语法和用法

  1. 在语法解释上 引用是别名,不开空间;指针是地址,需要开空间存放地址
  2. 引用必须初始化,而指针是否初始化均可
  3. 引用不能改变指向,而指针可以
  4. 引用相对更加安全,不存在空引用,难出现野引用
  5. 还要注意 sizeof++ 、 解引用访问等方面的区别

底层

在底层实现上我们对比汇编代码来查看,如下面会发现,在底层实现上实际是有空间的,因为 引用 是按照 指针 方式来实现的,这哥俩同根同源, 但语法解释引用是不开空间的 ,要分得清!!!

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	int& ra = a;
	ra = 20;
	int* pa = &a;
	*pa = 20;
	return 0;
}

在这里插入图片描述

内联函数

依然用于解决 C语言的缺陷

认识内联函数

我们知道调用函数是需要开销的,假如将下面的函数调用一百万次,那么将建立一百万个函数的额外栈空间,岂非是巨额开销?

int Add(int a, int b)
{
	return a + b;
}

那么 C 语言应该怎么解决这个问题?
答案是 宏定义 !因为宏是直接替换, 不需要建立栈空间 ;宏分为 宏常量 与 宏函数 ,宏常量我们非常清楚,那么宏函数是难点!

  1. 宏函数不是函数,没有参数概念
  2. 注意宏是预处理阶段整体替换,所以宏不存在末尾分号,如果将 末尾分号 替换进去本就不合适
  3. 用括号控制优先级,因为替换掉的可能是表达式,需要加括号

所以接下来就可以写出正确的宏函数了:

// 将 ADD(x, y) 替换为 ((x) + (y))
#define ADD(x, y) ((x) + (y))

对于 C 语言 来说:语法复杂,坑多不易控制;不能调试观察问题;没有类型安全检查

那么 C++ 不能容忍呀,因为回归问题本源,就是一百万个栈空间资源开销太大,所以追加关键字 inline ,这就是内联函数 的关键字

inline int Add(int a, int b)
{
	return a + b;
}

什么意思呢?就是在 inline 修饰下的函数 不需要建立额外栈空间 ,直接就地展开执行函数逻辑,所以内联函数可以提升程序运行的效率

内联函数特性

小一点的函数可以就地展开提升程序运行的效率,那大体量函数呢?就地展开会提升效率吗?并不会,因为将大体量函数就地展开会引发 代码膨胀 的问题

假如 func() 函数就是一个大体量的函数,拥有 100 行代码,且程序有一万个地方需要调用 func() ,那此时如果将 func() 函数内联起来,就会编译 1000000 行代码; 不使用内联 ,就会编译100 + 10000 行代码;也就是说内联之后会导致代码文件很大,引发 代码膨胀 的问题,导致需要编译相当长的时间

这里就是 inline 空间换时间的做法,但这里的空间并不是内存空间,而是文件空间, inline 对于编译器而言只是个建议,不同编译器关于 inline 实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略 inline 特性

内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求

所以编译器会识别只把小函数改为内联特性,大函数就算添加 inline 也不会内联

内联函数的坑

不可将内联函数的声明与定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到

那么现在问问大家为什么要将函数的声明与定义分离?其中有一个原因就是为了 防止链接错误 ;在一个项目里,我们写出一个函数要在其他文件里使用的话,如果将函数直接定义在 .h 文件,那么包含此 .h 文件的所有 .cpp 文件将会在程序链接阶段汇总在一起进入符号表,输出两份一样的符号从而报链接错误,所以声明与定义分离是很好的做法

但如果我就是想 将函数直接定义在 .h 头文件里 , 不想声明与定义分离 怎么办?很显然,咱就别让函数汇总进入符号表即可,那么这里就有两种做法:

  1. 大文件 情况:在函数前添加 static ,使用此关键字可使函数仅限在当前文件使用,换句话说,包含此 .h 头文件的 .cpp 文件各自的同名非重载函数是封闭在各自文件内的,不会进入符号表也不会出现连接错误!

  2. 小文件 情况:在函数名前添加 inline ,此关键字可以使调用此函数的地方直接利用函数体原地展开,也不会有符号表一说,自然不会报错

auto

这个关键字好用啊,比如一些 不好拼写的数据类型名字 或者说你也 不能明确知道某个确切的数据类型,因为好多数据类型被 typedef 不断改名,这时就可以使用 auto 关键字,直接自动定位成为确切的数据类型

#include <iostream>
using namespace std;
int main()
{
	int a = 10;
	auto b = a;
	cout << typeid(b).name() << endl;
	auto c = 'a';
	cout << typeid(c).name() << endl;
	return 0;
}

在这里插入图片描述

看起来是贼好用的关键字,推导的数据类型极为灵活,但要注意我们得有初始化的数据类型才能推导,像引用一样

使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型;因此 auto 并非是一种“类型”的声明,而是一个类型声明时的”占位符“,编译器在编译期会将auto替换为变量实际的类型

注意:

  1. auto 不能做函数参数,但现在似乎可以做函数的返回值
  2. auto 不能直接用来声明数组,切记

基于范围的for循环(C++11)

先来看代码:

#include <iostream>
using namespace std;
int main()
{
	int a[] = { 1,2,3,4,5,6,7,8,9,10 };

	for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
	{
		cout << a[i] << " ";
	}
	cout << endl;

	for (auto e : a)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

在这里插入图片描述

发现不仅可以用第一种 for 来遍历数组,第二种居然也可以

这个语法就叫范围 for ,它会自动依次取数组里的值赋值给变量 e ,自动迭代,自动判断结束,底层其实是迭代器,不必觉得奇怪,要多用这种范围 for 遍历数组,很好用,不过 e 是数组值的拷贝,要想改变数组里的值,得要使用 & 引用来完成:
在这里插入图片描述

与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环

注意:范围 for 的使用必须是明确的范围,像下面的情况就不可以 ,因为这里的 array 已经不再是数组,而是指针!

void TestFor(int array[])
{
	for(auto& e : array)
		cout << e << endl;
}

指针空值 nullptr (C++11)

我们以前表示空指针都是使用 NULL ,但是在 C++98 里有个 bug,看下面:

在这里插入图片描述

我们发现 NULL 居然匹配为 int 类型,这是因为 NULL 在被定义时为宏,本质就是 0 :

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void*)0 ,所以在 C++11 中,引入 nullptr ,可以更好的匹配类型

注意:

  1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的
  2. 在 C++11 中,sizeof(nullptr)sizeof((void*)0) 所占的字节数相同
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值