zai@TOC
C++入门
正如标题一样,顾名思义,本节内容主要目的就是快速入门C++这门语言,很多人就想问了,为什么C++这么难的语言可以快速入门呢?不会又是标题党吧,嘿你别说,还真不是,因为在看本节内容之前我是默认你是扎扎实实的学过一遍C语言的,只有有了一门编程语言的基础之后才可以快速入门其他一门语言,当然这门语言最好是C语言,C生万物不是说说而已,强烈不推荐没有学习C语言的直接学C++。
好了回来继续说我们的C++,C++是在C的基础上,容纳进去了面向对象的编程思想,并增加了许多有用的库,以及编程范式等,学习C当然也是对C++有一定帮助的,本节C++入门这节具体的目标:
- 补充C语言语法的不足,以及了解C++是如何对C语言设计不合理的地方进行优化的,例如:作用域方面,IO方面,函数方面,指针方面,宏方面等。
- 为后续类和对象打基础。
好了,说了这么多,那么开始本节内容的学习吧。
C++的关键字(C++98)
C++中63个关键字,C语言32个。
asm | do | if | return | try | continue |
---|---|---|---|---|---|
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
我们这里只列举一下,简单看一下不做具体解释,其实里面有很多C语言中学过的,但是也有很多可能看不懂的,没有关系,想一下当初学习C语言时一上来不也是看32个关键字一脸懵吗?之后慢慢都会理解的。
命名空间
命名空间
在C/C++中,变量、函数、以及后面要学习的类,都是大量存在的,这些函数变量类的名称如果都存在于全局作用域中,就会造成很多冲突。那么使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突和污染,那么命名空间的关键字叫做namespace
,名字起的也是非常容易理解。
看下面一段程序,理解一下什么是命名冲突:
#include <stdio.h>
#include <stdlib.h>
int rand = 0;
int main()
{
printf("%d", rand);
return 0;
}
直接编译的话可以发现编译器直接就报错了,rand重定义,便是我们所说的命名冲突,在stdlib
这个头文件中已经有了一个名叫rand的函数,我们这里又定义了一个rand变量,编译当然也就出错了。
命名空间定义
那么我们如何用命名空间来解决上面的冲突问题呢?其实很简单,我们只需要将我们自己定义的变量装到一个命名空间域里面即可。
namespace lzb
{
int rand = 0;
}
int main()
{
printf("%d", lzb::rand);
return 0;
}
这里我们自己定义的命名空间域,名字就叫做lzb,下去练习建议用自己的名字缩写即可,而一般开发中是用项目名字做命名空间名,
下面在使用我们自己的命名空间域内的东西时,使用了一个::
作用域限定符,就是用来指定使用哪个作用域中的变量函数等等。
- 一个命名空间就构成了一个新的作用域。
上面就是最基本的命名空间定义和使用的方式。
下面我们具体来看一下几种命名空间的特点:
1.正常的命名空间定义(常用)
namespace lzb
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
2.命名空间可以嵌套(常用)
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
3.命名空间是开放的,即可以随时把新的成员加入已有的命名空间中(常用)
也就是说编译器最后会将多个命名空间内的东西合并。
namespace A {
int a = 100;
int b = 200;
}
//将c添加到已有的命名空间A中
namespace A {
int c = 300;
}
4.命名空间中的函数 可以在“命名空间”外定义
namespace A {
int a=100;//变量
void func();
}
void A::func()//成员函数 在外部定义的时候 记得加作用域
{
//访问命名空间的数据不用加作用域
cout<<"func遍历a = "<<a<<endl;
}
void funb()//普通函数
{
cout<<"funb遍历a = "<<A::a<<endl;
}
void test06(){
A::func();
funb();
}
5.无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接(了解)
namespace{
int a = 10;
void func(){
cout<<"hello namespace"<<endl;
}
}
void test(){
//只能在当前源文件直接访问a 或 func
cout<<"a = "<<a<<endl;
func();
}
6.给命名空间起个别名(了解)
namespace veryLongName{
int a = 10;
void func(){
cout << "hello namespace" << endl;
}
}
void test(){
namespace shortName = veryLongName;
cout << "veryLongName::a : " << shortName::a << endl;
veryLongName::func();
shortName::func();
}
上面代码里涉及到一个cout
和endl
,先只知道cout
是C++中的输出,endl
是换行即可。
以上几种情况其实不必全部掌握,有些只做了解即可,将常用的嵌套、添加成员等最常用的理解即可。
命名空间使用
命名空间定义我们学会了,那么怎么使用呢?C++中提供了三种使用方式。
来看这样一个命名空间,看如何访问其成员,
namespace lzb
{
// 命名空间中可以定义变量/函数/类型
int a = 0;
int b = 1;
int Add(int left, int right)
{
return left + right;
}
namespace lzb2
{
int c = 100;
struct Node
{
struct Node* next;
int val;
};
}
}
- 加命名空间名称及作用域限定符
int main()
{
cout << lzb::a << lzb::b << endl;//访问a和b
cout << lzb::lzb2::c << endl;//访问c
return 0;
}
- 使用
using
将命名空间中某个成员引入
using lzb::a;
using lzb::lzb2::c;
int main()
{
cout << a << c << endl;
return 0;
}
- 使用
using namespace
命名空间名称引入(命名空间展开)
using namespace lzb;
using namespace lzb::lzb2;
int main()
{
cout << a << b << endl;
cout << c << endl;
return 0;
}
C++输入&输出
新生的婴儿会以自己独特的方式来向这个崭新的世界打招呼,C++刚出来之后,也是一个新的事物,那C++怎么来向这个美好的世界来生问候呢?
我们来看学习C++的第一段程序,
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}
和学习C语言时我们写的第一句代码一样,hello world!!!有没有勾起你一些初次接触编程时的回忆呢,所有程序员的第一段代码,也是有那么一点点浪漫的嘛。
言归正传,我们来看其中的输出语句,
说明:1. 使用
cout
标准输出对象(控制台)和
cin
标准输入对象(键盘),必须包含头文件,以及按命名空间规则使用
std
。
2.
cout
和
cin
是全局的流对象,
endl
是C++特殊的符号,表示换行,都包含在中。
3.
<<
是流插入运算符,
>>
是流提取运算符。
4. 使用C++输入输出更方便,不需要像
printf/scanf
输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
5. 实际上
cout
和
cin
分别是
ostream
和
istream
类型的对象,
>>
和
<<
也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。
注意:
> 早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用+std的方式。
下面可以自己写一下简单的输入输出代码熟悉一下即可,感受一下C++的输入输出比C语言方便在了哪里,
c++ #include <iostream> using namespace std; int main() { int a; double b; char c; // 可以自动识别变量的类型 cin >> a; cin >> b >> c; cout << a << endl; cout << b << " " << c << endl; return 0; }
关于
cout
和
cin
还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,实际上这些又用得不是很多,平时如果需要控制格式的话其实用
printf
是很方便的。
可以看一下这些格式,如果你想这样写的话,也可以记住。这里还有一篇推荐的博客,如果有需要可以点进去看: http://t.csdn.cn/r6ntO
# 缺省参数
## 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
c++ void Func(int a = 0) { cout << a << endl; } int main() { Func();//没有传参时,使用参数的默认值 Func(10);//传参时,使用指定实参 return 0; }
## 缺省参数分类
- 全缺省参数
c++ void Func(int a = 10, int b = 20, int c = 30) { cout << "a= " << a << endl; cout << "b= " << b << endl; cout << "c= " << c << endl; }
- 半缺省参数
c++ void Func(int a, int b = 10, int c = 20) { cout<<"a = "<<a<<endl; cout<<"b = "<<b<<endl; cout<<"c = "<<c<<endl; }
注意:
半缺省参数只能从右向左依次给出,不能间隔
- 缺省参数在函数的定义和声明中不能同时出现
因为如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值
缺省值必须为常量或者全局变量(一般为常量)
通常在声明处给缺省值。
函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,我国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
C++中其实就是解决函数名不能重名的问题,带来很大的不方便,例如两个整数相加和两个浮点数相加,如果非得定义成两个函数就有些过于麻烦了,像这种功能类似的函数当然还是同名很方便。
- 参数类型不同
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;
}
思考:为什么不设置返回值不同呢?
想一下在调用函数的时候,我们是不写返回值的,那编译器怎么能知道你要调用哪个函数呢。
C++函数重载的原理
上面我们只是了解了一下什么是函数重载,也知道了怎么用,但是还不够,因为我们不知道它是通过什么实现的,那么下面我们就来看一下函数重载的实现方法,以及为什么C语言不支持,C++是怎么进行优化的。
- C++函数重载底层原理是基于编译器的
name mangling
机制。
以下面两个函数为例,我们在linux环境下可以验证:
我们知道编译链接分为几个阶段,每个阶段做什么事情来回顾一下:
预处理阶段:删注释,头文件展开,宏替换,条件编译...
编译:语法检查,符号汇总,生成汇编代码...
汇编:形成符号表,生成二进制机器代码...
链接:合并段表,符号表的重定位和合并...
我们知道在链接时,要通过符号表来寻找函数等一些标识符的地址,那么符号表中的函数名是怎么定义的呢,其实C++的函数重载就是通过重新设计这个名称来实现的,举个例子:
我们先写出一段C语言代码来看一下C语言中的函数名修饰规则,我们是在Linux中来展示,
其中所看到的信息是用下面一句指令来看到的:
objdump -S 目标文件
这是C语言中的函数修饰规则,直接用函数名,而下面我们用同样的方法,看一下C++的修饰规则是什么样的,
可以看到,已经不是简单的函数名了,下面来解释一下这个命名规则,我们就以_Z3Fundd
这个为例吧:
其他函数名修饰规则也是同理,通过这种方法,在符号表中的函数名就有区别,在链接时也就可以根据你的函数参数来区别你要调用的是哪个函数,这就是C++函数重载的作用原理。
值得一提的是,不同的环境下的名字修饰规则并不相同,我们上面是用的linux下g++编译器来展示的,而在windows系统vs编译器中的命名规则要复杂的多,例如下面的示例:
所以我们一般都是用Linux下来演示,两者思想都是一样的,只是在实现上有差别。
引用
引用概念
引用不是新定义一个变量,而是给已经存在的空间取一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同一块空间。
比如一个形象的例子:李逵,在家叫铁牛,江湖人称黑旋风。虽然有多个名字,但是本质都是这个人。
引用方式:
类型 &引用变量名(对象名)=引用实体;
注意:引用类型必须和引用实体是同种类型的
int main()
{
int a = 10;
int& ra = a;
cout << a << endl;
cout << ra << endl;
}
类型不一致则会报错:
引用特性
- 引用变量在定义时必须初始化
- 一个变量可以有多个引用
- 引用在初始化时引用一个实体后,就不能再引用其他实体
常引用
如果想对常量进行引用,必须将引用变量也加上const
修饰,
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
//int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
}
对于常引用还有一种情况,
void TestConstRef()
{
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
cout << rd;
}
这时候有些类似于强制转换,加上常属性则可以引用不同类型的变量。
使用场景
1.做参数
最经典的例子就是两数交换,若按照之前的方法写就是用指针,而现在可以使用引用这一更方便的方式:
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
2.做返回值
首先要清楚一点,我们的函数为什么要使用引用来返回,
- 函数需要返回的值比较小的时候,返回时通过寄存器来暂时保存,但是寄存器只有4/8个字节,并且数量也很有限,
- 函数需要返回的值比较大的时候,直接返回其实是会形成临时变量的,
函数需要返回的值如果特别大的话,那么每次都要形成很大临时变量,造成的开销一定就很大。这时候返回指针当然是一种方式,但是我们今天学习了引用,那么引用可不可以呢?
看下面一段代码:
你会发现非常奇怪的一幕,为什么三次的ret打印出来都不一样呢,并且只有第一次的ret是正确的。
这就又要提及到之前的函数栈帧了,函数用引用做返回的是c的别名吗?我们看这段代码似乎是的,但实际上并不是这么简单,在编译器编译时,这段代码是会报出一个警告的,
所以实际上我们返回的是c的地址回来,而由于c是在函数Add中定义的,函数Add调用完就会销毁,将此段栈帧还给操作系统,下面再去访问实际上已经是非法访问了,同时我们也发现了,函数销毁并不是将那段空间数据清空,而仅仅是将使用权还给操作系统,所以第一次访问时访问到了正确的数据,但是下面又调用了其他函数,只要又有操作用了那段空间,其中的值就会改变,所以在后面两次访问到的值都发生了变化。
所以通过上面的分析就可以知道,如果需要返回一个较大的数据时,正确使用引用返回可以极大的避免空间的消耗,但是好看的玫瑰却是带刺的,用此方法实现起来缺尤为需要注意非法访问的问题。
总结:
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
传值和传引用效率的比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
上面我们已经分析了传值和传引用效率的问题,那么两者有多大差别呢。
- 作为函数参数
#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 < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
当传引用作为函数参数,和传值相比,两者效率看起来差了不是一星半点。
- 作为函数返回值
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 1000000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 1000000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
看起来传引用的效率还是非常高的,但是还是要提醒,传引用作返回值,慎用!
引用和指针的区别
引用和指针其实这两个玩意关系很微妙,
从语法层面上讲,引用就是一个别名,没有独立空间,和其引用体共用同一块空间。这是我们对引用的理解。
但是从底层上来看,引用实际上是有空间的,因为引用的底层就是用指针来实现的。
这个其实我们也可以观察到,
可以看到,汇编代码是一模一样的。
引用和指针的不同点
- 引用从概念上定义一个变量的别名,指针存储一个变量地址。
- 引用定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型的实体
- 没有NULL引用,但有空指针
- 在
sizeof
中含义不同:引用结果为引用类型大小,但指针始终是地址空间所占字节个数(32位平台下4个字节) - 引用自加即引用的实体加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但没有多级引用
- 访问实体方式不同,指针需要解引用,引用编译器自己处理
- 引用比指针使用起来更加安全
内联函数
概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
通常情况下,对于一些简短并且使用频繁的函数我们才使用内联函数,不要把所有函数都设为内联函数,具体原因后面会提到。
内联函数是否展开我们怎么能知道呢?如何去进行验证,我们先来看一个函数
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
Add(1, 2);
return 0;
}
这个函数很简短了吧,我们也将它定义成了内联函数,但是编译器有没有按照我说的去做呢,我们可以来验证一下,还是通过汇编代码的方式来看,正常情况下调用函数一定有一条call
指令,如果我们没有看到call
指令说明函数展开了,而不是去调用这个函数。
坏了,这编译器怎么回事,为什么没有按照我说的去做呢?
实际上编译器当前的版本是debug
版本,VS在debug
版本下默认内联函数是生效的,因为debug
版本本来就是调试版本,不会对代码做出优化。
下面可以在realease
版本下看一下,
可以看到确实没有call
指令,说明确实没有去调用函数,而是直接展开了。
但是另外还要补充一下,debug
版本下的内联函数是可以通过项目属性设置更改的,下图是VS2022下的演示,
特性
inline
是一种以空间换时间的做法,如果编译器将函数作为内联函数处理,在编译阶段会将函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了函数调用,提高程序运行效率。inline
对于编译器只是一个建议,不同编译器对于内联函数的实现机制并不完全相同,一般建议:将函数规模较小(其实就是函数体比较短,不同编译器实现不同,没有确定说法),不是递归,且频繁调用的函数使用inline修饰,否则编译器会忽略你的建议。
inline
不建议定义和声明分离,分离会导致链接错误。因为inline
被展开后,是没有函数地址的,最后链接通过符号表去找是找不到的。建议将内联函数之间写到头文件中。
对于这种短小使用频繁的函数为了减少消耗,在C++内联函数出现之前的解决方式是用宏函数,但是用宏去实现,又会有很多问题,
宏的优缺点:
优点:
- 增强代码复用性
- 提高性能
缺点:
- 不方便调试(宏是在预处理编译阶段进行替换)
- 导致代码可读性差,可维护性差,容易误用
- 没有类型安全的检查
C++有哪些可以代替宏?
常量定义:
const
enum
短小函数定义,换用内联函数
auto关键字
为什么要有auto
要想先简单提一下auto的作用,根据右边的参数自动推导类型,例如:
auto a =10;
int a=10;
上诉两种写法完全等价,这样看起来似乎auto
也没有什么价值,我还按原来那么写就可以了嘛。
那是因为我们现在所使用的类型还很简单,到后面我们会有一些名字特别长的情况,例如:
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
"橙子" },
{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
std::map<std::string, std::string>::iterator
这是一个类型,但是这类型太长了,很容易出错。可能有的同学会想到用typedef
来进行一下重命名,这确实是一种解决方式,但是还不够,这样在有些情况下又会引起新的问题:
typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。
auto简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
typeid
这个操作可以打印出变量的类型。
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto不能推导的场景
- auto不能作为函数的参数
- auto不能用来直接声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
基于范围的for循环
范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main()
{
int arr[5] = { 1,2,3,4,5 };
for (auto e : arr)
cout << e << ' ';
for (auto& e : arr)
cout << e << ' ';
return 0;
}
两种写法都可以很好的遍历数组,但是两种写法有什么区别呢,auto&是引用可以修改数组中的值,而只用值是改不了的。
int main()
{
int arr[5] = { 1,2,3,4,5 };
for (auto& e : arr)
e *= 2;
for (auto& e : arr)
cout << e << ' ';
return 0;
}
范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
图中的函数参数是一个指针,而不是数组,此时范围就是不确定的。
- 迭代的对象要实现++和==的操作
这个迭代器的概念以后再看。
指针空值nullptr
C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void )0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。