目录
3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。但是不允许同一个命名空间有相同名称的命名变量
1. 指向常量对象时,一定要使用“常量引用”,而不能是一般的引用。
2. “常量引用”可以指向一个非常量对象,但不允许用过该引用修改非常量对象的值。
3. 引用的类型必须和所引用的类型严格匹配,且不能与字面值或者某个表达式的计算结果绑定在一起,但是 “常量引用” 是例外(只要被引用的类型能够转换为常量引用的类型)。
一.C++关键字(C++98)
C++总计63个关键字,C语言32个关键字
asm | do | if | retun | 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++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都将作用于全局作用域中,可能会导致很多命名冲突。
使用命名空间的目的:对标识符和名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
#include <stdio.h>
#include <stdlib.h>
int rand = 10; //error:“rand”: 重定义;以前的定义是“函数”
int main()
{
printf("%d\n", rand);
return 0;
}
上述报错的原因,是因为在stdlib.h库里面中已经有rand定义了
要解决该问题,C++祖师爷定出来一个命名空间,是一个域
2.1 命名空间定义
定义命名空间,需要使用到 namespace 关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
1. 命名空间的普通定义
namespace GalaxyPokemon //GalaxyPokemon是命名空间的名称
{
// 命名空间中可以定义变量/函数/类型
//定义变量
int rand = 2024;
//定义函数
int Add(int left,int right)
{
return left + right;
}
//定义类型
struct Pokemon
{
struct Pokemon* pikaqiu;
int val;
};
}
2. 命名空间的嵌套定义
namespace GalaxyPokemon1 //定义一个名叫GalaxyPokemon1的命名空间
{
int a;
int b;
int Add(int left,int right)
{
return left + right;
}
namespace GalaxyPokemon2//嵌套一个名叫GalaxyPokemon2的命名空间
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。但是不允许同一个命名空间有相同名称的命名变量
ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.cpp
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. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
int main()
{
printf("%d\n",N1::Add(1,2)); //打印结果:3
printf("%d\n",N1::N2::Sub(1,2)); //打印结果:-1
printf("%d\n",N1::Mul(1,2)); //打印结果:2
}
注意:一个命名空间就定义了一个新的作用域,命名空间中所有内容都局限于该命名空间中。
2.2 命名空间的使用
1. 域作用限定符
命名空间名称::命名空间成员
域作用限定符为两个冒号::其作用是:通知编译器应从作用域限定符左侧的名字所示的作用域中寻找右侧那个名字,即指定访问哪个名字空间的哪个成员。
#include <stdio.h>
#include <stdlib.h>
int a = 0; //全局域
namespace GalaxyPokemon
{
int a = 1; //命名空间域
}
using namespace GalaxyPokemon; //展开了命名空间
int main()
{
int a = 2; //局部域
//默认局部搜索 -> 全局域 -> 展开了命名空间域(暴露到全局) or 指定访问命名空间域
printf("%d\n", a);//打印结果: 2
//指定访问命名空间域
printf("%d\n", GalaxyPokemon::a);//打印结果: 1
//加了::表示去左边的域里面搜索,::的左边是空白,空白就代表全局域
printf("%d\n", ::a); //打印结果: 0
return 0;
}
注意:展开命名空间,就等于暴露到全局,如果此时全局中有与命名空间相同的定义,那么就会报错
#include <stdio.h>
#include <stdlib.h>
int a = 0; //全局域
namespace GalaxyPokemon
{
int a = 1; //命名空间域
}
using namespace GalaxyPokemon; //展开了命名空间 = 暴露到全局
int main()
{
printf("%d\n", a);//打印错误: a不明确
return 0;
}
2.加命名空间名称及作用域限定符
符号“::”在C++中叫做作用域限定符,我们通过“命名空间名称::命名空间成员”便可以访问到命名空间中相应的成员。
#include <stdio.h>
namespace GalaxyPokemon
{
int a;
double b;
}
int main()
{
GalaxyPokemon::a = 10;//将命名空间中的成员a赋值为10
//打印命名空间中的成员a
printf("%d\n", GalaxyPokemon::a);//打印结果: 10
return 0;
}
3. 使用using将命名空间中的成员引入
我们还可以通过“using 命名空间名称::命名空间成员”的方式将命名空间中指定的成员引入。这样一来,在该语句之后的代码中就可以直接使用引入的成员变量了。
#include <stdio.h>
namespace GalaxyPokemon
{
int a;
}
using GalaxyPokemon::a; //将命名空间中的成员a引入
int main()
{
int a = 10; //将命名空间中的成员a赋值为10
printf("%d\n", a); //打印命名空间中的成员a
return 0;
}
4. 使用using namespace 命名空间名称引入
通过”using namespace 命名空间名称“将命名空间中的全部成员引入。这样一来,在该语句之后的代码中就可以直接使用该命名空间内的全部成员了。
#include <stdio.h>
namespace GalaxyPokemon
{
int a;
}
using namespace GalaxyPokemon;//将命名空间GalaxyPokemon的所有成员引入
int main()
{
a = 10;//将命名空间中的成员a赋值为10
printf("%d\n", a);//打印命名空间中的成员a
return 0;
}
5. 命名空间嵌套中的访问
有时候我们也需要进行嵌套定义多个相同的变量,可以使用“using 命名空间名称1::命名空间名称2::命名空间成员”进行访问
#include <stdio.h>
namespace GalaxyPokemon1 //定义一个名叫GalaxyPokemon1的命名空间
{
int a = 0;
int b;
int Add(int left,int right)
{
return left + right;
}
namespace GalaxyPokemon2//嵌套一个名叫GalaxyPokemon2的命名空间
{
int a = 1;
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
int main()
{
printf("%d\n", GalaxyPokemon1::a); //打印结果:0
printf("%d\n", GalaxyPokemon1::GalaxyPokemon2::a); //打印结果:1
printf("%d\n", GalaxyPokemon1::GalaxyPokemon2::Sub(5,3));//打印结果:2
return 0;
}
三. C++中的输入输出
我们在C语言中经常使用到输入和输出,但是在C++中的输入和输出跟C语言的不太一样
C++的输入输出不需要增加数据格式控制,它能自动识别类型
3.1 流插入运算符
#include <iostream>
int main()
{
// std是C++标准库的命名空间名,c++将标准库的定义实现都放到这个命名空间中
using namespace std;
//using std cout;//或者也可以这样
int x = 10;
double y = 11.1;
// <<流插入运算符
cout << "hello world!";//代表“hello world!”流向cout,cont可以理解成控制台
cout << "hello world!" << endl;//end1代表换行的意思
cout << "hello world!" << x << '\n' << endl; //代表“hello world!” x \n都流向控制台
cout << x << y << endl;//自动识别类型 打印结果 10 11.1
}
3.2 流提取运算符
#include <iostream>
int main()
{
// std是C++标准库的命名空间名,c++将标准库的定义实现都放到这个命名空间中
using namespace std;
//using std cin;
int x = 10;
double d = 11.11;
// >>流提取运算符
cin >> x >> d;//打印整型x 浮点数d
return 0;
}
在C语言中有标准输入输出函数scanf和printf,而在C++中有cin标准输入和cout标准输出。在C语言中使用scanf和printf函数,需要包含头文件stdio.h。在C++中使用cin和cout,需要包含头文件iostream以及std标准命名空间。
四. 缺省参数
4.1 缺省参数 概念
在声明或定义函数时,为函数的参数指定一个默认值。在调用该函数时,如果没有传参则采用该默认值,传参就使用指定的实参。
#include <iostream>
using namespace std;
void Print(int a = 0)
{
cout << a << endl;
}
int main()
{
Print();//没有指定实参,使用参数的默认值(打印0)
Print(10);//指定了实参,使用指定的实参(打印10)
return 0;
}
4.2 缺省参数 分类
全缺省参数 - 即函数的全部形参都设置为缺省参数
void Print(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Print();
Print(1);
//Print(1,,3);这种方法是错误的。必须从左往右依次给出,不能间隔着给。
Print(1,2);
Print(1,2,3);
}
半缺省参数 - 即函数的参数不全为缺省参数
void Print(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Print();
Print(1);
//Print(1,,3);这种方法是错误的。必须从左往右依次给出,不能间隔着给。
Print(1,2);
Print(1,2,3);
}
4.3 缺省参数 使用
比如创建一个栈,要给栈初始化空间,如果我知道需要插入多少个数据,那我可以给指定的空间,或者用define进行定义,但是会存在一定的缺陷
//动态申请空间的方法
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* pst, int defaultCapacity)
{
pst->a = (int*)malloc(sizeof(int) * defaultCapacity);
if (pst->a == NULL)
{
perror("malloc fail");
return;
}
pst->top = 0;
pst->capacity = defaultCapacity;
}
int main()
{
struct Stack st1;
StackInit(&st1, 100);//插入100个数据
return 0;
}
//#####################################################################
//define的方法
#define DEFAULT_CAPACITY 100
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* pst)
{
pst->a = (int*)malloc(sizeof(int) * DEFAULT_CAPACITY);
if (pst->a == NULL)
{
perror("malloc fail");
return;
}
pst->top = 0;
pst->capacity = DEFAULT_CAPACITY;
}
//用define没办法实现这种多组自定义的操作
int main()
{
struct Stack st1;
StackInit(&st1, 100);//插入100个数据
struct Stack st2;
StackInit(&st2);//不知道要插入多少数据
return 0;
}
当需要多个数据进行自定义的时候缺省值就显得尤为重要了,我想st1插入100个数据,st2插入20个数据,就可以用到缺省参数
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* pst, int defaultCapacity = 4)
{
pst->a = (int*)malloc(sizeof(int) * defaultCapacity);
if (pst->a == NULL)
{
perror("malloc fail");
return;
}
}
int main()
{
struct Stack st1;
StackInit(&st1, 100);//插入100个数据
struct Stack st2;
//此时用缺省值的方法,减少了扩容的消耗,也能实现多组数据自定义给值
StackInit(&st2);//不知道要插入多少数据
return 0;
}
4.4 缺省参数 注意事项
1.半缺省参数必须从右往左依次给出,不能间隔着给。
void Print(int a=10, int b= 30, int c) //报错:“Print”: 缺少形参 3 的默认实参
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
Print(1, 2); //报错
}
2.缺省参数不能在函数声明和定义中同时出现
//错误示例
//test.h
void Print(int a, int b, int c = 30);
//test.c
void Print(int a, int b, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
一般都是声明给,定义不给
3.缺省值必须是常量或者全局变量
//正确示例
int x = 30;//全局变量
void Print(int a, int b = 20, int c = x)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
五. 函数重载
5.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这 些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
5.2 函数重载的定义
1. 参数类型不同
#include<iostream>
using namespace std;
// 1、参数类型不同
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;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
return 0;
}
2. 参数个数不同
#include<iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(10);
return 0;
}
3. 参数类型顺序不同
#include<iostream>
using namespace std;
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;
}
int main()
{
f(10, 'a');
f('a', 10);
return 0;
}
函数重载的原理(名字修饰)
为什么C++支持函数重载,而C语言不支持函数重载呢?
我们知道,一个C/C++程序要运行起来都需要经历以下几个阶段:预处理、编译、汇编、链接。
我们知道,在编译阶段会将程序中的每个源文件的全局范围的变量符号分别进行汇总。在汇编阶段会给每个源文件汇总出来的符号分配一个地址(若符号只是一个声明,则给其分配一个无意义的地址),然后分别生成一个符号表。最后在链接期间会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(重定位)。
在C语言中,汇编阶段进行符号汇总时,一个函数汇总后的符号就是其函数名,所以当汇总时发现多个相同的函数符号时,编译器便会报错。而C++在进行符号汇总时,对函数的名字修饰做了改动,函数汇总出的符号不再单单是函数的函数名,而是通过其参数的类型和个数以及顺序等信息汇总出 一个符号,这样一来,就算是函数名相同的函数,只要其参数的类型或参数的个数或参数的顺序不同,那么汇总出来的符号也就不同了。
注:不同编译器下,对函数名的修饰不同,但都是一样的。
总结:
- 1、C语言不能支持重载,是因为同名函数没办法区分。而C++是通过函数修饰规则来区分的,只要函数的形参列表不同,修饰出来的名字就不一样,也就支持了重载。
- 2、另外我们也理解了,为什么函数重载要求参数不同,根返回值没关系。
六. 引用
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体。
void QuoteTest()
{
int a = 10;
int& ra = a;//定义引用类型
printf("%p\n", &a); //打印结果:a的地址
printf("%p\n", &ra); //ra是a的别名。 打印结果:a的地址
}
int main()
{
QuoteTest();
return 0;
}
注意:引用类型必须和引用实体是同种类型的
6.2 引用的特性
1. 引用在定义时必须初始化
int main()
{
int a = 10;
int& ra;
printf("%p\n", &ra); //报错:"ra"必须初始化引用
return 0;
}
2. 一个变量可以有多个引用
int main()
{
int a = 10;
int& b = a;
int& c = a;
printf("%d\n", a); //打印结果:10
printf("%d\n", b); //打印结果:10
printf("%d\n", c); //打印结果:10
return 0;
}
3. 引用一旦引用一个实体,再不能引用其他实体
int main()
{
int a = 10;
int b = 20;
int& c = a;
int& c = b;
c = b; //这个的意思是将b的值拷贝给c
printf("%d\n", c); //“c”: 重定义;多次初始化
return 0;
}
6.3 常引用
上面提到,引用类型必须和引用实体是同种类型的。但是仅仅是同种类型,还不能保证能够引用成功,我们若用一个普通引用类型去引用其对应的类型,但该类型被const所修饰,那么引用将不会成功。
void TestConstRef()
{
const int a = 10;
int& ra = a; //错误,a为常量,权限不能放大
//const int& ra = a;//正确
int& b = 10; // //错误,10为常量
//const int& b = 10;//正确
int x = 0;
int& y = x; //正确,是权限的平移
//const int&z = x; //正确,是权限的缩小,但是不能通过z去改变x,可以通过y去改变x
double d = 12.34;
int& rd = d; //错误,会产生个int的临时变量,临时变量具有常属性,属于权限的放大
//但是相同类型不会产生临时变量
//const int& rd = d;//正确
}
1. 指向常量对象时,一定要使用“常量引用”,而不能是一般的引用。
因为不允许直接为常量赋值,当然也就不能通过引用去改变常量。因此直接规定当引用一个常量时,必须使用“常量引用”。
const int a = 1024;
const int &b = a; // 正确:引用及其对应的对象都是常量
b = 42; // 错误:b是对常量的引用,不能被用作修改它所绑定的对象
int &c = a; // 错误:试图让一个非常量引用指向一个常量对象
2. “常量引用”可以指向一个非常量对象,但不允许用过该引用修改非常量对象的值。
“常量引用”仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
int i = 42;
int &r1 = i; // 普通引用指向非常量对象 i
const int &r2 = i; // 常量引用也绑定非常量对象 i
r1 = 0; // 正确,r1并非常量引用
r2 = 0; // 错误:r2是一个常量引用
r2 绑定非常量整数 i
是合法的行为。然而不允许通过 r2 修改 i 的值。尽管如此,i
的值仍然允许通过其他途径修改,既可以直接给 i
赋值,也可以通过像 r1 一样绑定到 i
的其他引用来修改。
3. 引用的类型必须和所引用的类型严格匹配,且不能与字面值或者某个表达式的计算结果绑定在一起,但是 “常量引用” 是例外(只要被引用的类型能够转换为常量引用的类型)。
int i = 42;
const int &r1 = i; // 正确:指向非常量对象
const int &r2 = 42; // 正确:r2 是一个常量引用
const int &r3 = r1 * 2; // 正确:r3 是一个常量引用
int &r4 = r1 * 2; // 错误:r4 是一个普通的非常量引用
double dval = 3.14;
const int &r1 = dval; //也是正确的
此处"const int &r1 = dval"编译器实际上相当于执行了下列语句:引用和原 dval它俩已经不是同一个地址了
const int temp = dval; // 生成一个临时的整型常量
const int &r1 = temp; // 让 r1 绑定这个临时量
6.4 引用的使用
1. 引用做参数
还记得C语言中的交换函数,学习C语言的时候经常用交换函数来说明传值和传址的区别。现在我们学习了引用,可以不用指针作为形参了:
void Swap(int& a, int& b)
{
int temp = 0;
temp = a;
a = b;
b = temp;
}
int main()
{
int x = 10;
int y = 20;
Swap(x,y);
printf("%d %d",x,y); //打印结果:20 10
return 0;
}
这里的a,b是进行了引用,也就是x,y的别名,当a,b进行了调换,那么本体x,y也会进行调换了。
2. 引用做返回值
修改返回值 + 获取返回值
传值作为返回值,会形成一个临时变量,传给ret,相当于两次拷贝,n拷贝给临时变量。临时变量拷贝给ret。
引用做返回值就不会在生成临时变量了 ,减少了拷贝,提高了效率
int& Count()
{
static int n = 0;
n++;
return n; //返回的是n的别名
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
附加:对n不加static的分析
附加:对ret也进行引用的分析
using namespace std;
int& Count(int x)
{
int n = x;
n++;
// ...
return n;
}
int main()
{
int ret = Count(10); //这个ret只是n的别名的拷贝,当Count函数空间销毁,不影响ret
int& ret = Count(10); //ret是Count函数返回值n的别名的别名,ret和n的地址是相同的,当Count函数空间销毁,会影响ret
cout << ret << endl;
rand();
cout << ret << endl;
return 0;
}
代码解析:
注意:如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给系统了,则必须使用传值返回。
3. 引用和指针的区别
在语法概念上,引用就是一个别名,没有独立的空间,其和引用实体共用同一块空间。
int main()
{
int a = 10;
//在语法上,这里给a这块空间取了一个别名,没有新开空间
int& ra = a;
ra = 20;
//在语法上,这里定义了一个pa指针,开辟了4个字节(32位平台)的空间,用于存储a的地址
int* pa = &a;
*pa = 20;
return 0;
}
但是在底层实现上,引用实际是有空间的:
从汇编角度来看,引用的底层实现也是类似指针存地址的方式来处理的。
引用和指针的区别(面试好考):
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。
- 引用比指针使用起来相对更安全。
七. 内联函数
7.1 内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数的使用可以提升程序的运行效率。
我们可以通过观察调用普通函数和内联函数的汇编代码来进一步查看其优势:
int Add(int a, int b)
{
return a + b;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
从汇编代码中可以看出,内联函数调用时并没有调用函数这个过程的汇编指令。
7.2 内联函数的特性
- inline是一种以空间换时间的做法,省了去调用函数的额外开销。由于内联函数会在调用的位置展开,所以代码很长或者有递归的函数不适宜作为内联函数。频繁调用的小函数建议定义成内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有递归等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了链接就会找不到。如下
// F.h
#include <iostream>
using namespace std;
inline void f(int i); //声明
// F.cpp
#include "F.h"
void f(int i) //定义
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
【面试题】 宏的优缺点?
优点: 1.增强代码的复用性。 2.提高性能。
缺点: 1.不方便调试宏。(因为预编译阶段进行了替换) 2.导致代码可读性差,可维护性差,容易误用。 3.没有类型安全的检查 。
C++有哪些技术替代宏?
1. 常量定义 换用const enum 2. 短小函数定义 换用内联函数
八. auto关键字(C++11)
8.1 类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
#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::iterator 是一个类型,但是该类型太长了,特别容易写错。这时候就可以通过auto去定义
8.2 auto简介
在早期的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
#include <iostream>
using namespace std;
double Fun()
{
return 3.14;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'A';
auto d = Fun();
//打印变量b,c,d的类型
cout << typeid(b).name() << endl;//打印结果为int
cout << typeid(c).name() << endl;//打印结果为char
cout << typeid(d).name() << endl;//打印结果为double
return 0;
}
注意:使用auto变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此,auto并非是一种“类型”的声明,而是一个类型声明的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
8.3 auto的使用细则
1. auto与指针和引用结合
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
auto b = &a; //自动推导出b的类型为int*
auto* c = &a; //指定必须是指针
auto& d = a; //自动推导出d的类型为int
//打印变量b,c,d的类型
//typeid(变量名).name()能打印类型
cout << typeid(b).name() << endl;//打印结果为int*
cout << typeid(c).name() << endl;//打印结果为int*
cout << typeid(d).name() << endl;//打印结果为int
return 0;
}
注意:用auto声明引用时必须加&,否则创建的只是与实体类型相同的普通变量。
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
auto a = 1, b = 2; //正确
auto c = 3, d = 4.0; //编译器报错:“auto”必须始终推导为同一类型
return 0;
}
2. auto不能推导的场景
1. auto不能作为函数的参数
以下代码编译失败,auto不能作为形参类型,因为编译器无法对x的实际类型进行推导。
void TestAuto(auto x)
{}
2. auto不能直接用来声明数组
int main()
{
int a[] = { 1, 2, 3 };
auto b[] = { 4, 5, 6 };//error
return 0;
}
3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有 lambda表达式等进行配合使用。
九. 基于范围的for循环(C++11)
若是在C++98中我们要遍历一个数组,可以按照以下方式:
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//将数组元素值全部乘以2
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
arr[i] *= 2;
}
//打印数组中的所有元素
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
cout << arr[i] << " ";
}
cout << endl;
以上方式也是我们C语言中所用的遍历数组的方式,但对于一个有范围的集合而言,循环是多余的,有时还容易犯错。因此C++11中引入了基于范围的for循环。
9.1 范围for的语法
基于范围的for循环的语法:
for(范围内用于迭代的变量:被迭代的范围)
int arr[] = { 1,2,3,4,5,6,7,8,9 };
//适用于数组
//依次取数组中数据赋值给e
//自动迭代,自动判断结束
for (auto e : arr)
{
cout << e << endl;
}
将数组元素值全部乘以2
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//for (auto e : arr) //这个方法是不行的,只是代表把arr的值拷贝给e,而e是个局部变量,
//出了第一个for循环e就销毁了,在第二个for循环e是个新的变量,不再是之前的那个变量了。
//e的改变不会影响数组的内容
for (auto& e : arr) //这里的auto也可以是int,只是为了自动化所以用的auto
{
e *= 2;
}
//打印数组中的所有元素
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
注意:与普通循环类似,可用continue来结束本次循环,也可以用break来跳出整个循环。
9.2 范围for的使用条件
1. for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])//这里传的是指针了,数组传参传的是首元素的地址
{
for(auto& e : array)
cout<< e <<endl;
}
2. 迭代的对象要实现++和==的操作
(关于迭代器这个问题,以后会讲,现在提一下,没办法 讲清楚,现在大家了解一下就可以了)
十. 指针空值nullptr
1. C++98中的指针空值
在良好的C/C++编程习惯中,在声明一个变量的同时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误。比如未初始化的指针,如果一个指针没有合法的指向,我们基本都是按如下方式对其进行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其实是一个宏,在传统的C头文件(stddef.h)中可以看到如下代码:
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
可以看到,NULL可能被定义为字面常量0,也可能被定义为无类型指针(void*)的常量。但是不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,例如:
#include <iostream>
using namespace std;
void Fun(int p)
{
cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
cout << "Fun(int*)" << endl;
}
int main()
{
Fun(0); //打印结果为 Fun(int)
Fun(NULL); //打印结果为 Fun(int)
Fun((int*)NULL); //打印结果为 Fun(int*)
Fun((nullptr); //打印结果为 Fun(int*)
return 0;
}
程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0,Fun(NULL)最终调用的是Fun(int p)函数。
注:在C++98中字面常量0,既可以是一个整型数字,也可以是无类型的指针(void*)常量,但编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制转换。
2. C++11中的指针空值
对于C++98中的问题,C++11引入了关键字nullptr。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。
- 在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后序表示指针空值时建议最好使用nullptr。