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++ 怎么做:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
可以看到除了几个可能认识的关键字之外,其他的都是生面孔,那么接下来就用此程序一脚踢开 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;
}
看上述代码,这就将两个 x
在 main
函数的外面封装了起来,但是先要理解下面的问题:
- 命名空间就是在全局域里开荒,将被开荒出来的范围进行所属,变为 指定域
- 两个
x
还是全局变量,只是被封装到命名空间的域里面,做了个名字的隔离,并且两个x
不会冲突,所以命名空间封装的还是全局的东西 - 和全局域局部域要区分开,这哥俩对自己域内的变量既会影响生命周期,又会影响访问;而命名空间域内变量只能定义在全局,不会影响生命周期,只会影响访问,是对冲突变量进行隔离
- 编译器的 默认 搜索变量原则: 默认搜索变量原则是在 不指定域 内,也就是 当前局部域 --> 全局域 ,如果在这两个域内均搜索不到,就会报错,因为命名空间虽然在全局域里,但他俩并不是同一个域;所以你又该如何访问上述
test1
和test2
里的两个x
呢?
域作用限定符 ::
就有用处了,看下图:
这样做就变相解决了编译器的变量搜索问题,相当于 去指定域内 搜索变量,所以对上述 rand
也就有了解决之法:
上图可以看到这似乎就完美解决了命名冲突的问题,由于库里的 rand
是库函数,也就是函数指针,所以咱打印出它的地址即可
关于命名空间还有几点需要注意:
- 命名空间的空间名可以重复,编译器会将重复的空间名合并为同一个域,此时域内不可有重名哦
- 命名空间也可以定义结构,函数等等,因为这些玩意均有可能会冲突
- 命名空间可以套娃,也就是命名空间里嵌套命名空间,只不过访问的时候也需要不断嵌套,例如:
Bit::z111::Push();
这里Bit
是最外层的命名空间,z111
是第二层命名空间 - 在指定命名空间内的 结构体 时,需要注意将 域作用限定符
::
放在结构体名称前而不是 struct 关键字前
展开命名空间
咱们看回第一个程序:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
using namespace std;
就是将名为 std
的命名空间域在全局域内展开,理解为将 std
命名空间的 访问权限打开,在全局范围内使用此域内的变量函数等等,比如 cout
, endl
, <<
, >>
等等
而 std
又是什么呢?std
就是 C++ 内所有的库命名空间,通俗点讲,就是要想使用 C++ 库里的东西,就要展开或指定 std
命名空间,所以也可以像下面这样写:
如果将 std
全展开势必会出现新的命名冲突情况,可是将库内变量函数等一次次指定又显得特别臃肿繁琐,所以咱可以像下面这样将使用频率高的进行展开即可:
#include <iostream>
using std::cout;
using std::endl;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
C++ 的输入输出
针对上面的第一个程序而言,我们发现有 cout
和 endl
之类的字眼,那他们是什么呢?其实从代码运行结果上看 cout << "Hello World!" << endl;
的作用就类似于 printf("Hello World!\n");
一样将 Hello World! 输出在屏幕上并换行,以现在的知识储备是无法讲清 cout
, cin
, <<
, >>
之类的标识符的,所以目前就着其作用可以简单聊一聊
- 使用
cout
标准输出对象(控制台)和cin
标准输入对象(键盘)时,必须包含<iostream>
头文件
以及按命名空间使用方法使用std
cout
和cin
是全局的流对象,endl
是特殊的 C++ 符号,表示 换行输出,他们都包含在包含<iostream>
头文件中<<
与cout
配合的时候是 流插入 运算符,>>
与cin
配合的时候是 流提取 运算符。- 使用 C++ 输入输出更方便,不需要像
printf/scanf
输出输入时那样,需要手动控制格式,C++ 的输入输出可以自动识别变量类型 - 实际上
cout
和cin
分别是ostream
和istream
类型的对象,>>
和<<
也涉及运算符重载等知识
缺省参数
有了上面的认知,相信大家在一定程度上或多或少地感知到 C++ 的不同之处,似乎比 C 语言更加严谨,也可以说是填补了 C 语言的缺点,更加好用,缺省参数 就是鲜明的案例
缺省参数,也可以叫做 默认参数,是声明或定义函数时为函数的参数指定一个缺省值,在调用该函数时,如果 没有指定实参则采用该形参的缺省值 ,否则使用指定的实参
注意:
- 缺省值必须是 常量 或者 全局变量
- 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;
}
需要注意以下几点:
- 如上代码可以看到全缺省就是 将此函数的所有的形参 都给上缺省值
- 在我们调用的时候发现,传实参的顺序是不能变的,只能从左往右依次传参;
- 由于是 全缺省 实现,所以可以只传一部分实参;但顺序依旧是从左往右依次传参!也就是说如果只传一个实参就是传给了左边第一个形参,传两个实参就分别对应左边的两个形参,如上图代码结果,以此类推,所以是不能跳跃着传参的
半缺省
半缺省 就是对于函数参数 只给出部分缺省值 ,半缺省参数 必须从右往左依次来给出 ,不能间隔着给,如下图:
// 正确示范
#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
源文件,那么此时 缺省值应该放在声明处
我们知道程序想要运行分为 预处理、编译、汇编、链接 和 运行 阶段
- 预处理 :这个阶段的作用是 展开头文件、宏替换、条件编译 和 去掉注释 等等,所以
.h
头文件并不参与编译阶段 - 编译 :检查语法,语法无误再进一步生成汇编代码
- 汇编 :把汇编代码转成二进制机器码
- 链接 :将
.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
这是什么呢?仔细看会发现他俩并不相同,只是最后一个 @
前的 H
和 D
换了位置,这就是 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;
}
在这里可以看到, a
和 b
就是 同一块空间 ,地址均为 00EFFA6C
,也就是说修改任何一方,另一方也一定会发生改变,且 a
和 b
的值一定是一样的,就是因为 在同一块空间 的缘故:
#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;
}
有趣的是:如果你有需要,别名可以 在同一块空间 一直 无限取 ;且 可以给别名取别名 ,别绕进去咯,上面代码里 b
是 a
的别名,同样也可以给 b
取别名叫做 c
引用使用原则
- 引用必须初始化! 就是说你在定义一个引用变量时必须是某个变量的别名,不可以像这样
int& b;
这是大忌!就像现实中你能说出不依附于人或物的外号吗? - 引用不可改变指向 ,也就是说你初始化为
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++ 不一样
引用和指针的区别
既然引用不能完全替代指针,那它们之间的主要区别是什么呢?
语法和用法
- 在语法解释上 引用是别名,不开空间;指针是地址,需要开空间存放地址
- 引用必须初始化,而指针是否初始化均可
- 引用不能改变指向,而指针可以
- 引用相对更加安全,不存在空引用,难出现野引用
- 还要注意
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 语言应该怎么解决这个问题?
答案是 宏定义 !因为宏是直接替换, 不需要建立栈空间 ;宏分为 宏常量 与 宏函数 ,宏常量我们非常清楚,那么宏函数是难点!
- 宏函数不是函数,没有参数概念
- 注意宏是预处理阶段整体替换,所以宏不存在末尾分号,如果将 末尾分号 替换进去本就不合适
- 用括号控制优先级,因为替换掉的可能是表达式,需要加括号
所以接下来就可以写出正确的宏函数了:
// 将 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
头文件里 , 不想声明与定义分离 怎么办?很显然,咱就别让函数汇总进入符号表即可,那么这里就有两种做法:
-
大文件 情况:在函数前添加
static
,使用此关键字可使函数仅限在当前文件使用,换句话说,包含此.h
头文件的.cpp
文件各自的同名非重载函数是封闭在各自文件内的,不会进入符号表也不会出现连接错误! -
小文件 情况:在函数名前添加
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替换为变量实际的类型
注意:
auto
不能做函数参数,但现在似乎可以做函数的返回值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
,可以更好的匹配类型
注意:
- 在使用
nullptr
表示指针空值时,不需要包含头文件,因为nullptr
是 C++11 作为新关键字引入的 - 在 C++11 中,
sizeof(nullptr)
与sizeof((void*)0)
所占的字节数相同 - 为了提高代码的健壮性,在后续表示指针空值时建议最好使用
nullptr