目录
🙊 c++ 版 “Hello World!”🙊
以下代码是 c++ 版本的 Hello World 程序:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
这段代码中,namespace 代表命名空间,cout 代表一个 IO 流,而 endl 代表换行,接下来我们进行详细介绍。
🙊命名空间🙊
💖 命名空间的概念
在 C/C++ 中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。如下面一段代码:
#include <stdio.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
我们运行上面的程序是没有问题的,但是我们再包一个头文件如下:
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{
printf("%d\n", rand);
return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
我们发现编译后报错了
为什么加了一个头文件编译器会报错呢?因为我们知道在 C 语言的库中有一个取随机值函数叫 rand,这就造成了名字冲突,为了解决类似的冲突问题,就提出了命名空间 namespace 来解决。
💖 命名空间使用
我们知道同一个域里面不能有同名的变量,但是不同的域可以有同名的变量。如下面一段代码:
#include<stdio.h>
#include <stdlib.h>
// 域
// 局部域/全局域:1、使用 2、生命周期
int a = 2;
void f1()
{
int a = 0;
printf("%d\n", a);
printf("%d\n",::a); // ::域作用限定符
}
int main()
{
printf("%d\n", a);
f1();
return 0;
}
定义一个全局变量 a = 2,函数 f1 中有一个局部变量 a = 0,当我们使用 printf 函数打印 a 的时候,由于局部优先的原则,我们打印出来的是 0,如果想打印全局变量,就需要加域作用限定符 :: ,即加了域作用限定符后,:: 左边为空,代表全局。
如果有这样一种场景,一个头文件定义了一个链表叫 Node,另一个头文件定义了一个链表也叫 Node,然后 test.cpp 中包含了这两个头文件。
编译之后结果如下:
结果显示类型重定义,因为头文件会展开,展开之后名字相同就会造成这种重定义现象,此时添加命名空间就会解决这个问题,命名空间定义为一个域,就是在全局域的基础上又加了一个命名空间域,命名空间域只影响使用,而不影响声明周期。如果上面两个 Node 各自在不同的命名空间域,那就不会造成重定义的问题。
由于编译器不会到命名空间中找 Node,而是去局部或者全局找,想要去命名空间找 Node,就需要指定命名空间,在命名空间域里寻找 Node。代码如下:
#include "test.h"
#include "test1.h"
int main()
{
struct potato1::Node node1;
struct potato2::Node node2;
return 0;
}
编译发现不会冲突。
以上说明命名空间可以解决类型或变量重名的问题。
注: 命名空间也可以多层嵌套
💖 全局展开
如果我们定义了一个命名空间,在使用的时候需要指定命名空间,如果不想每次用的时候都指定,就可以进行全局展开。默认情况下编译器不会去命名空间里面搜索,但是全局展开以后,编译器就会去命名空间里面搜索。
#include "test.h"
#include "test1.h"
using namespace potato1;
int main()
{
struct Node node1;
struct Node node2;
return 0;
}
我们写的代码和库里面某些代码也可能会发生冲突,所以 c++ 把标准库里全都被放入到了一个命名空间 std (c++ 标准库的命名空间) 中。这也正是我们开头写 using namespace std; 的原因。但是这种写法并不好,因为 std 是标准库专门建立起的一道防线,防止自己定义的东西和库里面的冲突,如果使用命名空间展开,就会造成冲突问题。所以一般情况下不建议直接进行全局展开。 但是平时练习可以全部展开。那么有没有一种方式解决这个问题呢?
💖 部分展开
如果不想使用全局展开,可以使用部分展开,代码如下:
#include <iostream>
using std::cout;
using std::endl;
int main()
{
cout << "hello world" << endl;
int i = 0;
std::cin >> i;
return 0;
}
总结:
1、平时写一些练习程序、不太会出现冲突的用全局展开
2、实际开发项目或工程中,指定命名空间访问、将常用的部分展开
3、命名空间是在编译的时候影响寻找的规则
🙊c++ 输入 & 输出 🙊
💖 使用说明
说明:
- 使用 cout 标准输出对象 ( 控制台 ) 和 cin 标准输入对象 ( 键盘 ) 时,必须包含 < iostream > 头文件以及按命名空间使用方法使用 std。
- cout 和 cin 是全局的流对象,endl 是特殊的 C++ 符号,表示换行输出,他们都包含在包含 < iostream > 头文件中。
- << 是流插入运算符,>> 是流提取运算符。
- 使用 C++ 输入输出更方便,不需要像 printf / scanf 输入输出时那样,需要手动控制格式。C++ 的输入输出可以自动识别变量类型。
- 实际上 cout 和 cin 分别是 ostream 和 istream 类型的对象,>> 和 << 也涉及运算符重载等知识,
这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习 IO 流用法及原理。
💖 注意事项
注意:
早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带 .h ;旧编译器 ( vc 6.0 ) 中还支持 <iostream.h> 格式,后续编译器已不支持,因此推荐使用 < iostream > + std 的方式。
#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;
}
💖 std命名空间的使用惯例
std 是 C++ 标准库的命名空间,如何展开 std 使用更合理呢?
- 在日常练习中,建议直接 using namespace std 即可,这样就很方便。
- using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型 / 对象 / 函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像 std::cout 这样使用时指定命名空间 + using std::cout 展开常用的库对象 / 类型等方式。
🙊缺省参数🙊
💖 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。如下面这段代码:
void func(int a = 0)
{
cout << a << endl;
}
int main()
{
func();
func(1);
return 0;
}
如果将 1 传过去,形参就使用 1,如果不传实参,形参就使用 0。
💖 缺省参数分类
💖 全缺省参数
全缺省参数就是可以不传参数,也可以传部分参数,也可以传全部参数,如下面代码:
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
Func(1,2,3);
Func(1,2);
Func(1);
Func();
return 0;
}
💖 半缺省参数
半缺省也叫部分缺省,就是给一部分缺省参数,必须从右往左连续缺省,代码如下:
void Func(int a, int b = 10, int c = 20)
// void Func(int a = 10, int b, int c = 20) //不允许这样写
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
int main()
{
Func(1,2,3);
Func(1,2);
Func(1);
//Func(); //部分缺省不可以不传
return 0;
}
注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给
- 缺省参数不能在函数声明和定义中同时出现,如果同时出现,在不传参的时候编译器不知道该用哪个缺省值,推荐在声明的时候给缺省值
- 缺省值必须是常量或者全局变量
- C 语言不支持缺省值(编译器不支持)
//a.h
void Func(int a = 10);
// a.cpp
void Func(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该
用那个缺省值。
🙊函数重载🙊
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
💖 函数重载概念
函数重载是函数的一种特殊情况,C 语言不允许同名函数的存在, C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表 (参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
💖 函数重载各种情况
函数重载有参数类型不同、参数个数不同、参数类型顺序不同几种重载方式。如下面一段代码所示:
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
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()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
函数重载会不会导致程序运行变慢呢?
答案是不会,因为如果是程序运行的时候才去匹配类型肯定会变慢,事实上在程序编译的时候就已经完成了函数重载,源代码执行编译变成指令,函数调用会被变成一个 call 指令 + 函数地址,执行 call 指令的时候,就跳到对应的函数地址,所以该程序在编译链接的时候就确定了调用的是哪个函数。而不会影响运行速度,只会影响编译速度。
那么编译器怎么识别是哪个函数呢?
因为相比 c 语言,c++ 对函数名进行了修饰,看如下代码:
int Add(int left, int right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
return 0;
}
针对上述代码,程序编译完成后,会将 Add 函数调用转换成指令,即 call + Add 的地址,再通过汇编指令调用。
而如何去找地址呢?不同平台有不同的修饰规则,这里主要介绍 linux 平台下的命名修饰规则。代码如下:
//_Z3Addii
int Add(int left, int right)
{
return left + right;
}
Add 函数被修饰为了 _Z3Addii,其中 _Z3 是前缀,3 是名字长度,ii 表示参数类型的首字母,如果是两个整型这里就为 ii,如果是两个 double 这里就是 dd。
由于在调用和编译时都对函数名进行处理和修饰。当编译完成以后,得到对应的函数地址,调用函数的时候用修饰完以后的函数名去找编译完成后生成的对应函数的地址。
因为函数参数类型不同、参数个数不同等导致经过修饰后的函数名不同,这样就区分开了同名函数。
💖 c++ 支持函数重载的原理
在 C / C++ 中,一个程序要运行起来,需要经历预处理、编译、汇编、链接这几个过程。C / C++ 在编译链接的时候,会生成符号表。不过 C 语言和 C++ 生成的符号表是不一样的。C语言的符号表直接是函数名 + 地址,而 C++ 的符号表是经过修饰后的函数名 + 地址。所以 C 语言不会支持同名函数,也就是不支持函数重载,而 C++ 是支持函数重载的。
🙊引用🙊
💖 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
类型& 引用变量名(对象名) = 引用实体;代码如下:
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
注意: 引用类型必须和引用实体是同种类型的。
💖 引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体,就是不能改变引用的指向
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
💖 常引用
如下面代码所示:
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
指针和引用在赋值或者初始化时,权限可以缩小但是不能放大,一个变量加了 const 修饰以后,变量变为只读,再用引用进行接收时也需要加 const 修饰。而赋值和只读不涉及权限问题,一个 const int 类型的变量可以赋值给 int 类型的变量。
再看下一段代码:
int Count()
{
int n = 0;
++n;
return n;
}
int main()
{
int& ret = Count();
return 0;
}
这段代码会报错,因为 Count 是传值返回,返回的不是 n,而是一个临时变量,而临时变量有常性,相当于加了 const 修饰,所以这里如果想用引用接收,应该加 const 进行修饰。
看下面一段代码:
int main()
{
int i = 0;
const double& b = i;
return 0;
}
由于 b 是 double 类型,而 i 是 int 类型,在类型转换的时候, i 会产生一个 double 类型的临时变量,由于临时变量具有常性,所以需要在 double 加一个 const 加以限制才可以完成赋值。
💖 使用场景
💖 做参数
引用可以做输出型参数,就是形参的改变可以影响实参,以前我们用指针可以实现形参的改变影响实参,现在可以使用引用实现。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
💖 做返回值
如下面一段代码,c++ 规定不是直接返回就需要产生一个临时变量做返回值。
int Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
首先建立 main 函数的栈帧,里面有一个未赋值的变量 ret ,然后建立 Count 函数的栈帧,临时变量 n 存在 Count 函数栈帧中,如果要返回 n 的话,不能直接将 n 拷贝给 ret,因为 Count 函数已经调用完成了,Count 函数创立的栈帧被销毁,由于 n 在栈帧里面,n 的使用权还给操作系统不能再访问,如果想要返回,就需要创建一个临时变量,将 n 拷贝给临时变量,再将临时变量作为返回值拷贝给 ret 变量。如果临时变量比较小通常用寄存器充当,如果是结构体,就在 main 函数栈帧里面开辟好,再进行拷贝。
再看下面的代码:
int Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
如果加了 static ,变量 n 存在于静态区,栈帧销毁不影响 n,此时编译器也需要用临时变量将静态区的 n 拷贝给临时变量再返回给 main 函数中的变量 ret。所以无论栈帧销毁与否,都会用临时变量进行返回。
为什么不先返回再销毁?
因为实际调用流程就是先调用 Count 函数,调用完函数后将为 Count 函数创建的栈帧销毁再回到上一层 main 函数的栈帧,再执行赋值。
第二个场景的传值返回,中间会产生一个 int 类型的临时变量,如果使用引用返回,这个过程又是怎样的?
看如下代码:
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
使用引用返回,返回时产生的是 n 的别名,也就是 n 本身,就减少了拷贝的过程。所以出了作用域,返回变量仍然存在,就使用引用返回,否则就使用传值返回。
再看下面一段程序的运行结果是什么:
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;
cout << "Add(1,2) is :" << ret << endl;
return 0;
}
运行结果如下:
分析:
该程序是一个错误的程序,才导致出现随机值的问题,add 第一次调用, c 的值为 3,出了 add 的作用域后栈帧销毁,但是栈帧销毁函数结束,原来存放 c 的空间还在,只是空间的使用权还给操作系统,此时用引用返回 c 的别名,ret 又是 c 的别名,所以此时 ret 就是 add 栈帧里面为 c 创建的空间的别名,栈帧销毁,c 创建的空间可能被清理,也可能还没有被清理,再去调用同一个函数,建立相同大小的栈帧,c 就在同一个位置,且被修改成为其他值。再去访问 ret,这个值就变成了这个值。因为 cout 也是一个函数调用,打印 ret 时,先去被销毁栈帧的位置寻找被销毁之前 c 空间里面的值。此时这个值就可能被改变,这也是产生随机值的原因。
所以如果函数返回时,除了函数的作用域,如果返回对象还在(还没还给系统),如静态变量,全局变量在上一层栈帧未被销毁的变量、malloc 申请的变量等。则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
💖 引用和指针的区别
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体- 没有 NULL 引用,但有 NULL 指针
- 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
- 从语法上来说,引用是变量的别名,并没有开辟新的空间,而指针开辟了一块空间存放变量的地址。从汇编层面讲,引用是通过指针实现的。
🙊 内联函数 🙊
💖 宏替换的缺陷
在 C 语言中经常使用宏来代替常量,也可以对频繁调用的小函数使用宏优化,因为宏是在预处理阶段完成替换的,并没有执行时的开销,并且因为代码量小,也不会造成代码堆积。但是无论是宏常量还是宏函数,都不能调试,因为宏在预处理的时候就被替换掉了,注意宏只是简单替换,并没有类型安全的检查。而有些场景下使用宏会很复杂,容易出错。如要实现 Add 的宏函数:
#define ADD(x, y) ((x) + (y))
int main()
{
cout << ADD(100, 200) << endl;
return 0;
}
💖 内联函数的概念
而 C++ 针对为了减少函数调用开销,这里的空间指的不是程序运行过程中占内存,而是编译出来的指令 (可执行程序),然后又可以在一定程度上替代宏,避免宏的出错,从而设计出了内联函数 。以 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
💖 内联函数的特性
1、 inline 是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2、 inline对于编译器而言只是一个建议,不同编译器关于 inline 实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略 inline 的特性。下图为**
《C++prime》** 第五版关于 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 中被引用
编译后不同的 cpp 文件会被编译成不同的 obj 文件,而在一个函数中使用其他文件里的函数,需要找到其函数的地址,但是内联函数是不进符号表的,也就是说在链接过程中,在符号表里面找不到内联函数的地址。内联函数是编译的时候建立栈帧在调用的地方进行展开。
🙊 auto关键字 🙊
💖 类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
1、 类型难于拼写
2、 含义不明确导致容易出错
#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 给类型取别名,比如:
#include <string>
#include <map>
typedef std::map<std::string, std::string> Map;
int main()
{
Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} };
Map::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
使用 typedef 给类型取别名确实可以简化代码,但是 typedef 有会遇到新的难题:
typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
运行发现 p1 失败,p2 成功,因为 char* 被 typedef 后,由于 const 修饰的是 p1,就变成了 char const p1*,因为 const 变量只有一次初始化的机会,必须被初始化,所以这里就会报错。而第二个 const 修饰的是 *p2,实际上是 int * const * p2 的二级指针。
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此 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;
}
【注意】
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种 “ 类型 ” 的声明,而是一个类型声明时的“占位符”,编译器在编译期会将 auto 替换为变量实际的类型。
💖 auto的使用细则
1、 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;
}
2、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
💖 auto不能推导的场景
1、 auto 不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2、 auto 不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
3、为了避免与 C++ 98 中的 auto 发生混淆,C++ 11 只保留了 auto 作为类型指示符的用法
4、auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有 lambda 表达式等进行配合使用。
🙊 基于范围的for循环(C++11) 🙊
💖 范围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 循环后的括号由冒号 “ :” 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
自动依次取数组中的数据取别名给 e 对象,自动判断结束。
注意:
与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。
🙊 指针空值nullptr(C++11) 🙊
在良好的 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 可能被定义为字面常量 0,如果定义了 c++ ,在 c++ 中 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;
}
程序本意是想通过 f ( NULL ) 调用指针版本的 f ( int )* 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖。
在 C++98 中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *) 0。
注意:
- 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++ 11 作为新关键字引入的。
- 在 C++ 11 中,sizeof ( nullptr ) 与 sizeof ( ( void* ) 0 ) 所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr。