? ? ? ? ? ? ? ? ? ? ? ? ? ?
目录? ? ? ? ? ? ? ? ? ? ?
什么是C++及历史
C++关键字
?命名空间
命名空间定义
普通命名空间
嵌套的命名空间
同一个工程中允许存在多个相同名称的命名空间
命名空间使用
错误的使用
加命名空间名称及作用域限定符
?使用using将命名空间中成员引入
使用using namespace 命名空间名称引入
std命名空间
C++输入&输出
cout
?cin
?缺省参数
缺省参数概念
?缺省参数分类
全缺省参数
半缺省参数(部分给缺省值)
函数重载
函数重载概念
函数参数个数不同
函数参数类型不同
函数参数顺序不同
思考题
函数名修饰规则
extern "C"
引用
引用概念
引用的使用
引用特性
权限
权限扩大
权限不变
权限缩小
?引用使用场景
做参数
做返回值?
?下面代码输出什么?
传值、传引用效率比较
传值、传引用传参性能比较
值和引用的作为返回值类型的性能比较
引用和指针的区别
引用和指针的不同点
内联函数
概念
Debug版本内联函数配置
?内联函数特性
面试题
内联函数缺陷
auto关键字(C++11)
auto使用细则
auto与指针和引用结合起来使用
?在同一行定义多个变量
auto不能推导的场景
auto不能作为函数的参数
auto不能直接用来声明数组
其次
基于范围的for循环(C++11)
范围for用法
范围for使用条件
指针空值
什么是C++及历史
? ? ? ?C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。 ? ? ? 1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
总结:C++出现是为了弥补C语言的不足,可以说是完善C语言的;C++是C语言的超集。
? ? ? ?1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
C++关键字
C++总计63个关键字,C语言32个关键字。
在这里并不是把关键字都介绍,而是简单的先了解一下。以后我会经常写博客,结合具体场景详细介绍。
?命名空间
? ? ? ?在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
命名空间定义
? ? ? ? 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
举个栗子:
普通命名空间
namespace cyq //以cyq为命名空间的名称
{
//命名空间中,即可以存变量,也可以存函数
int a = 1;
int Add(int x, int y)
{
return x + y;
}
}
嵌套的命名空间
namespace cyq //以cyq为命名空间的名称
{
//命名空间中,即可以存变量,也可以存函数
int a = 1;
int Add(int x, int y)
{
return x + y;
}
namespace wmm //以wmm为命名空间的名称
{
int a = 1;
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
}
}
? ? ? ?注意,虽然cyq和wmm两个命名空间中都有定义变量a,但是在这里并不会造成命名冲突。因为他们在不同的命名空间中,属于不同的作用域。这就避免了命名冲突或命名污染。
同一个工程中允许存在多个相同名称的命名空间 ?
namespace cyq //以cyq为命名空间的名称
{
//命名空间中,即可以存变量,也可以存函数
int a = 1;
int Add(int x, int y)
{
return x + y;
}
namespace wmm //以wmm为命名空间的名称
{
int a = 1;
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
}
}
namespace cyq
{
//int a = 10; //由于编译器会把名称相同的命名空间合并到一起,所以在这里会和上面的a造成冲突
int b = 10;
int Sub(int x, int y)
{
return x - y;
}
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
命名空间使用
错误的使用
namespace cyq
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
printf("%d\n", a);
return 0;
}
错误运行结果:?
命名空间使用有三种方式:
加命名空间名称及作用域限定符
namespace cyq
{
int a = 1;
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
printf("%d\n", cyq::a);
return 0;
}
运行结果:
? ? ? ? ?
?使用using将命名空间中成员引入
namespace cyq
{
int a = 10;
int Add(int x, int y)
{
return x + y;
}
}
using cyq::a;
int main()
{
printf("%d\n", a);
return 0;
}
运行结果:
使用using namespace 命名空间名称引入
namespace cyq
{
int a = 10;
int Add(int x, int y)
{
return x + y;
}
}
using namespace cyq;
int main()
{
printf("%d\n", a);
return 0;
}
运行结果:
std命名空间
? ? ? ? ? 通过以上就可以明白为什么我们写C++程序为什么总是写:using namespace std;?实际上C++标准命名都存在一个叫std的命名空间中,比如我们经常使用的:cout、cin。
? ? ? ? ?由于我们不能直接去访问。所以将命名空间展开就行了。不过使用using namespace std; 会让std这个命名空间失去了价值,所以在大工程项目中会采用using std::cout;??using std::cin;的写法。不过我们在平常练习就使用using namespace std; 就好了。
#include<iostream>// i表示输入,o表示输出,stream表示流
using namespace std;
C++输入&输出
cout
? ? ? ? ? <<:官方名字流插入。也可以叫流输出。cout和C语言中的printf有些相似,可以打印输出值。不同的是,cout在输出是不用指定类型。使用C++输入输出更方便,不需增加数据格式控制,比如:整形--%d,字符--%c
举个栗子:
int main()
{
int a = 10;
char ch = 'a';
cout << a << endl;
cout << ch << endl;
cout << "hello world" << endl;
return 0;
}
endl:是换行的意思,和'\n'的作用是一样的。
运行结果:
?cin
? ? >>:官方名字叫流提取,也可以叫流输入。cin和C语言中的scanf有些相似,可以输入值。不同的是,cin在输入是不用指定类型。使用C++输入输出更方便,不需增加数据格式控制,比如:整形--%d,字符--%c
举个栗子:
int main()
{
int a = 10;
char ch = 'a';
cin >> a >> ch;
cout << a << endl;
cout << ch << endl;
return 0;
}
? ??
?缺省参数
缺省参数概念
? ? ? ?缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
举个栗子:
void f(int a = 0)
{
cout << a << endl;
}
int main()
{
f(); //没有传参数时,使用默认参数值
f(10); //传参数时,使用指定的实参
return 0;
}
输出结果:
? ? ? ? ??
?缺省参数分类
全缺省参数
int f(int a = 1,int b = 2,int c = 3)
{
cout << a << endl;
cout << a << endl;
cout << a << endl;
}
半缺省参数(部分给缺省值)
int f(int a ,int b = 2,int c = 3)
{
cout << a << endl;
cout << a << endl;
cout << a << endl;
}
注意:1、半缺省参数是从右往左给,不能间隔着给。
? ? ? ? ? ?2、传参数时,是从左往右进行传参,没有缺省参数的变量必须要传参。
? ? ? ? ? ?3、缺省参数不能在函数声明和定义中同时出现。(一般缺省参数写到函数声明中)
? ? ? ? ? ?4、缺省值必须是常量或者全局变量。
? ? ? ? ? ?5、C编译器不支持。
错误栗子:
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
个缺省值
函数重载
函数重载概念
? ? ? ?函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
函数参数个数不同
void f(int x, int y)
{
cout << x << endl;
cout << y << endl;
}
void f(int x)
{
cout << x << endl;
}
函数参数类型不同
void f(int x, int y)
{
cout << x << endl;
cout << y << endl;
}
void f(double x, double y)
{
cout << x << endl;
cout << y << endl;
}
函数参数顺序不同
void f(double x, int y)
{
cout << x << endl;
cout << y << endl;
}
void f(int x, double y)
{
cout << x << endl;
cout << y << endl;
}
思考题
下面两个函数构成函数重载吗?
short Add(short left, short right)
{
return left+right;
}
int Add(short left, short right)
{
return left+right;
}
答案是:不构成。函数重载与函数的返回值类型无关,只与函数参数的类型、顺序、个数有关。
当然,当函数重载和缺省函数使用不当会出现报错,类似于下面情况:
void f(int a = 0)
{
cout << a << endl;
}
void f(int a ,int b = 2,int c = 3)
{
cout << a << endl;
cout << a << endl;
cout << a << endl;
}
int main()
{
f();
f(10);
return 0;
}
错误结果:
?因为在这里f(10),可以调用上面两个函数,这时候就构成冲突了。
f()只能调用函数:void f(int a = 0);所以它没问题。
函数名修饰规则
思考一下:为什么C++支持函数重载,而C语言不支持函数重载呢? ? ? ? ? 由于目前我对linux环境还不太熟练,等以后我会单独写一篇关于函数重载的的博客~? 在这里我就简单解释一下。
我们知道C/C++程序要运行起来要进行预处理、编译、汇编、链接几个阶段。
? ? ? ? ?
在链接时,C语言需要找到对应定义函数的地址,如果出现这种情况:
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
int main()
{
return 0;
}
? ? ? ? 我们知道C语言定义函数时,函数之间命名不能一样,否则在链接(其实在编译阶段生成的汇编代码阶段就出错了,汇编代码在这时就已经冲突了)时就会造成冲突,C语言是直接拿函数名去寻找对应函数定义的地址的,这时候会找到两个Add函数的地址,这时候就会出错了。
gcc编译后如下:
? ? ???
? ? ? ? 但是C++就不一样了,因为C++支持函数重载,它在链接时并不会直接拿函数名去寻找函数定义的地址。就算定义的几个函数名相同,只要他们参数的类型或顺序或个数不同就行。
在Linux环境下我们来看一下他们的函数名修饰规则(VS环境下修饰太过复杂,在这里我们用linux环境下来解释)。
我们看到经过这样的指令,程序竟然通过了。
g++ test.c
? ? ? ? ? ? ? ? ? ? ? ?
?那我们就来看看linux环境下函数名是怎么修饰的。
在Linux环境下,敲出这样的指令,运行我们在上面写的代码:
g++ -S test.c -o test.i
生成文件如下:
? ? ? ? ? ? ? ? ? ? ?
?接着我们就查看test.i文件里面的函数名修饰的汇编代码;
函数->int Add(int x, int y):
? ? ? ? ? ? ? ? ? ? ? ??? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
函数->double Add(double x, double y):
? ? ? ? ? ? ? ? ? ? ? ??
总结一下:我们发现两个函数名虽然相同,但是他们经过函数名修饰后就不一样了,这样程序在链接阶段不会因为函数名重复而找不到对应的地址了。
?int Add(int x, int y):_Z3Addii
double Add(double x, double y):_Z3Adddd
Linux环境下函数名修饰规则:_Z+函数名长度+函数名+参数类型首字母(对于一些指针类型的会加个P再加上参数首字母,当然还有别的情况,但是大致情况就是上面的一套规则)。
extern "C"
? ? ? ? 有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
? ? ? ?总结:意思是C++程序如果要想运行C的程序,在函数前加extern "C"来告诉编译器,用C的风格来运行程序。由于篇幅问题,我会在另写一篇博客用工程项目(并涉及到静态库)来介绍ertern "C"的具体使用。
引用
引用概念
? ? ? ? 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
引用的使用
int main()
{
int a = 1;
int& b = a;
printf("%p\n", &a);
printf("%p\n", &b);
return 0;
}
b就是a的别名,b和a指向同一块空间。b和a的地址相同!
? ? ? ? ?
? ?注意:引用类型必须和引用实体是同种类型的。
引用特性
1. 引用在定义时必须初始化 2. 一个变量可以有多个引用 3. 引用一旦引用一个实体,再不能引用其他实体? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?4、引用引用的实体必须是变量(不能是常量、define修饰的常量)
举个栗子:
#define n 10
int main()
{
int a = 1;
//int& ra; // err 1. 引用在定义时必须初始化
int& ra = a;//right
int& rra = ra;
int& rrra = a; //2. 一个变量可以有多个引用
//int& b = n; //err ?4、引用引用的实体必须是变量
return 0;
}
第三种情况:3. 引用一旦引用一个实体,再不能引用其他实体? ?
我们思考一下下面代码:ra是b的别名,还是b的值赋值给了ra?
int main()
{
int a = 1;
int b = 2;
int& ra = a;
ra = b;
cout << ra << endl;
return 0;
}
运行结果:
? ? ? ? ?
?我们发现b的值赋值给了ra,因为引用一旦引用一个实体,再不能引用其他实体。
权限
权限扩大
int main()
{
const int a = 1;
int& ra = a;
return 0;
}
这种写法是错误的,a是可读的,引用后ra是可读可写的,属于权限放大问题,不正确!
权限不变
int main()
{
const int a = 1;
const int& ra = a;
return 0;
}
正确。a和ra都是可读的。
权限缩小
int main()
{
int a = 1;
const int& ra = a;
a = 2;
return 0;
}
正确。a是可读可写的,引用后ra是只读的,权限缩小是成立的。
在这里,a是依然可以改变,而ra不行。
? ? ?
?引用使用场景
做参数
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
int main()
{
int a = 10;
int b = 20;
cout << "转换前:" << "a=" << a <<" " << "b=" << b << endl;
Swap(a, b);
cout << "转换后:" << "a=" << a << " " << "b=" << b << endl;
return 0;
}
运行结果:
做返回值?
int& Count()
{
static int sum=0;
int n = 0;
while (n <= 100)
{
sum += n;
n++;
}
return sum;
}
int main()
{
int ret = Count();
cout << ret << endl;
return 0;
}
运行结果:
? ??
?下面代码输出什么?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
我们的期望的结果是3,但实际上结果如下:
?这是为什么?
? ? ?int& ret = Add(1, 2);,ret是Add(1,2);的别名,又因为函数是传引用返回,如果再进行一次调用函数Add(3,4);这时候这个函数栈帧建立在刚刚销毁的栈帧上,并且计算c的值被改成7了,而且ret是Add的别名,ret 和Add指向同一块空间。最后ret会到Add()函数销毁的栈帧去找c,从而打印的值变成了7。
实际上这种代码写法是错误的!
? ? ? ? 注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。 ?
如果对上面的代码进行简单的改造:
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
printf("hello world!\n");
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
运行结果:
? ? ? ?发现ret的结果是随机值,因为printf("hello world!\n"); 的函数栈帧(只要这个函数栈帧开辟足够大,能覆盖原来Add函数的栈帧空间)覆盖了原来销毁后Add函数c的空间,这时候空间内容被清理修改了,这时候就会生成随机值·。
总结:注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
传值、传引用效率比较
? ? ? ?以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
传值、传引用传参性能比较
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;
}
int main()
{
TestRefAndValue();
return 0;
}
运行结果:
值和引用的作为返回值类型的性能比较
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 < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
运行结果:
? ? ? ?通过以上两种情况比较传值、传引用的效率相差挺大,尤其是传的是大对象时结果会更加明显。?
引用和指针的区别
引用在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。 引用在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
我们来看下面代码底层怎么实现的:
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
? ? ? ? ? ? ? ? ? ? ?
? ? ? ?实现方式都是先将[a]的地址给寄存器esp,然后寄存器地址的那块空间的值传给对应的[ra] 、[pa] 。dword ptr:dword是双字,一个word是两个字节,dword是4个字节。
它们的汇编代码基本一致,引用的底层实际上是指针实现的。
? ? ? ? ? ? ? ? ??
? ? ? ? 一样道理,它们的汇编代码也基本一样。先把dword ptr [ra] 内容给寄存器eax,14h代表十六进制,转换成十进制是20.。dword ptr [eax],14h ,这时候20给对应的空间内。
引用和指针的不同点
1. 引用在定义时必须初始化,指针没有要求 2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任? ? ? 何一个同类型实体 3. 没有NULL引用,但有NULL指针 4. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数? ? ? ? (32位平台下占4个字节) 5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 6. 有多级指针,但是没有多级引用 7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理 8. 引用比指针使用起来相对更安全
内联函数
概念
? ? ? ?以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
举个栗子:
内联函数修饰的函数:
inline int Add(int x, int y)
{
int tmp = x + y;
return tmp;
}
int main()
{
int ret = Add(3, 4);
cout << ret << endl;
return 0;
}
普通函数:
int Add(int x, int y)
{
int tmp = x + y;
return tmp;
}
int main()
{
int ret = Add(3, 4);
cout << ret << endl;
return 0;
}
Debug版本内联函数配置
那这两种方式有什么区别呢?结果运行后都是一样的。
查看方式: 1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add 2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认? ? ? 不会对代码进行优化,以下给出vs2019的设置方式)
内联函数在relase版本下汇编代码:
?普通函数在relase版本下汇编代码:
Debug(未配置的)内联函数汇编代码:
? ???
? ? ? ?其实在这里relase把非内联函数优化成内联函数了,所以我们要想进一步观察需要在Debug版本下进行配置(Debug版本没办法直接看内联函数优化的汇编代码)。?
步骤如下:
1、
? ? ? ? ? ??
2、
3、
4、
接着我们看普用函数的汇编代码:
debug版本内联函数汇编代码:?
? ? ?这时候我们就可以看到没有call Add()函数的指令了,于是就减小了栈空间开销,优化了代码。
?内联函数特性
?1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归? ? ? ?的函数不适宜使用作为内联函数。 2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有? ? ? ? 循环/递归等等,编译器优化时会忽略掉内联。 3. 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 2. 函数定义 换用内联函数
内联函数缺陷
? ? ? ? 比如一段内联函数代码,在1000处调用,那么它会在1000个地方展开,假如这段函数代码有10行指令,展开后就是10000行。
而普通函数不会展开,只是调用1000次,相当于最后只有1010行指令,空间大大节省了,当然,对于现代计算机而言,空间消耗很多时候是可以接受的。
本质上,内联函数是以空间换时间的做法。
auto关键字(C++11)
? ? ? ?在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量(指定当前作用域,出了作用域该变量就销毁了),但遗憾的是一直没有人去使用它。 ? ? ? C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
使用栗子:
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a; //自动推导b类型 int
auto c = 'a'; //自动推导c类型 char
auto e = &a;//自动推导e类型 int*
auto* f = &a;//自动推导f类型 int*
auto d = TestAuto();//自动推导d类型 int
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
运行结果:
注意:使用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不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
auto不能直接用来声明数组
void TestAuto()
{
int a[] = { 1,2,3 }; //right
auto b[] = { 4,5,6 }; //err
}
其次
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
auto在实际中最常见的优势用法就是C++11提供的新式for循环,还有lambda表达式等进行配合使用。
基于范围的for循环(C++11)
范围for用法
C++98要遍历一个数组时;
void TestFor()
{
int a[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
{
a[i] *= 2;
}
for (int* p = a; p < a + sizeof(a) / sizeof(a[0]); ++p)
{
cout << *p << endl;
}
}
C++学习近年来比较新的语言,像python、go语言,也引进了范围for的概念。
? ? ? ?对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
举个栗子:
void TestFor()
{
int a[] = { 1, 2, 3, 4, 5 };
for (auto& e : a)
{
e *= 2;
}
for (auto e : a)
{
cout << e << " ";
}
}
int main()
{
TestFor();
return 0;
}
运行结果:
?注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
范围for使用条件
for循环迭代的范围必须是确定的 ? ? ? ?对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
传参过去的数组不能用范围for。
指针空值
? ? ? ? 在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr() { int* p1 = NULL; int* p2 = 0; // …… }
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由此可见NULL在C++中是0.不免会在一些场景下出现问题。
举个栗子:
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;
}
运行结果:
我们发现f(NULL)调用的是函数void f(int);? 实际上我们期望调用的是void f(int*);
因为C++中NULL被替换成了0,所以调用了函数void f(int);?
注意: 1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。 2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。 3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。(C++中)
|