目录
一、C++概念
(一)C++是什么
(二)C++的历史
(三)C++参考文档
二、第一个C++程序
三、C++的域
四、命名空间 namespace
(一)namespace 的作用
(二)namespace 的定义
(三)namespace 的使用
1、使用【域作用限定符::】对命名空间内容进行引用
2、使用 using 展开整个命名空间
3、使用 using 展开命名空间中的某变量/函数/结构
五、C++的输入与输出
(一)标准输入与输出
(二)流运算符
(三)换行
(四)std::cout 和 std::cin 与 printf 和 scanf 的对比
六、缺省参数
七、函数重载
八、引用
(一)引用的概念和定义
1、概念
2、定义
3、引用 与 typedef 和 define 的区别
(二)引用的特性
1、引用在定义时必须初始化
2、一个变量可以有多个引用
3、引用过一个实体后,不能改变指向
(三)引用的使用
(四)const引用
1、const引用与const对象
2、临时对象触发权限放大
3、const引用的使用
(五)指针和引用的关系
九、inline 内联函数
十、nullptr
十一、有意思的点
(一)越界访问不一定报错
(二)引用与指针的底层
一、C++概念
(一)C++是什么
C++本意为祖师爷本贾尼不满足C语言所存在的缺陷而做的拓展。
(二)C++的历史
其中大字体的为大版本更新,小的为小版本。
(三)C++参考文档
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/
说明:
第一个链接不是C++官方文档,标准也只更新到C++11,但是以头文件形式呈现,内容比较易看好懂;
后两个链接分别是C++官方文档的中文版和英文版,信息很全,更新到了最新的C++标准,但是相比第一个不那么易看;
几个文档各有优势,需要结合着使用。
二、第一个C++程序
#include<iostream>
using namespace std;
int main()
{
cout << "Hello World" << endl;
return 0;
}
结果如下所示:
若要看懂上面的代码,需要先学习几个知识点。
三、C++的域
C++中的域有函数局部域,全局域,命名空间域,类域。
域的影响:
① 域做到了名字隔离,不同域可以定义同名变量;
② 影响生命周期:
全局域中的变量/函数的生命周期是随着程序结束而结束的,而函数局部域中的变量/函数的生命周期则是出了该作用域就进行销毁;
命名空间域和类域不影响变量的生命周期。
③ 影响编译查找逻辑:使用变量/函数的时候,如果不限定域名,则会先使用局部域,再搜索全局域。
四、命名空间 namespace
(一)namespace 的作用
在C/C++中,有众多的变量、函数和类都存在于全局作用域中,而这些变量、函数和类的名字很有可能会与包含的头文件中的变量、函数和类的名字起冲突,因为同一作用域中不能有相同名字的定义;
使用命名空间的目的是对标识的名称进行本地化,以避免命名冲突或者名字污,namespace关键字的出现就是针对这一问题的。
(二)namespace 的定义
namespace只能定义在全局域中,其中的变量、函数、类型也属于全局域的。
命名空间 namespace 的定义类似于结构体的定义,使用方法如下:
如下代码演示:
int i = 10;
namespace zyb
{
int i = 100;
}
namespace 本质上是创建了一个域来解决问题,因为不同域可以定义相同名字的变量/函数/类型,如上代码的变量 i 就不存在命名冲突的问题。
注意:
① 在工作中一般使用项目名称作为命名空间名;
② 只能定义在全局域中,因为本身就是全局的;
③ namespace 能嵌套定义,目的是为了解决命名空间中的同名问题,如下代码所示:
namespace zyb
{
namespace a
{
int i = 10;
}
namespace a
{
struct i
{
int num1;
double num2;
};
}
}
④ 命名空间域和全局域的生命周期一样,都是随着程序结束而结束。
⑥ 多文件使用同名的命名空间域不会冲突,系统会把同名的命名空间合并在一起。
C++标准库已经放在std(standard)的命名空间中,在使用C++标准输入输出时需要使用作用域限定符来指定函数的使用。
(三)namespace 的使用
想要使用命名空间域中的变量/函数/类型时,不能直接写他们的变量/函数/类型的名,这样机器在编译查找时会默认先在函数局部域中搜索,再查找全局域中是否存在想要使用的变量/函数/类型,并不会主动去命名空间域中查找,所以需要指定使用命名空间域中的变量/函数/类型。
命名空间的使用有三种方法:
1、使用【域作用限定符::】对命名空间内容进行引用
【域作用限定符 ::】,为两个冒号。
注意:
① 使用命名空间域的变量/函数:命名空间名::变量/函数名;
使用命名空间中的结构体:struct 命名空间名::结构体名,如下示例:
namespace zyb
{
namespace a
{
int i = 10;
}
namespace a
{
struct i
{
int num1;
double num2;
};
}
}
int main()
{
zyb::a::i = 20;//命名空间名::变量名
struct zyb::a::i s1 = {s1.num1 = 10, s1.num2 = 1.2f};
//struct 命名空间名::结构体名
}
总结:只在名字前面写明【命名空间名::】。
②若【域作用限定符 :: 】的左边什么都没写,就默认是全局域;
③在项目中推荐使用这种方法。
2、使用 using 展开整个命名空间
在全局域中使用 using 展开命名空间的全部内容。
需要大量使用命名空间中的变量/函数/类型,且没有命名冲突的时候使用,因为展开命名空间相当于把命名空间中的内容暴露在全局域中,此时命名空间域的内容变成了全局域的内容,机器在编译查找时就会找到该变量/函数/类型。
使用方法如下:
如下代码所示:
namespace zyb
{
namespace a
{
int i = 10;
}
namespace a
{
struct ii
{
int num1;
double num2;
};
}
}
using namespace zyb::a;
int main()
{
i = 20;//命名空间名::变量名
ii s1 = {s1.num1 = 10, s1.num2 = 1.2f};//struct 命名空间名::结构体名
}
使用特点: 使用方便,但有命名冲突的风险。
注意:
① 展开命名空间与展开头文件是不同的东西,展开头文件是在预处理阶段把头文件的内容拷贝过来;而展开命名空间是把命名空间的内容暴露在全局域中。
② 在项目中不推荐使用,因为很可能会产生命名冲突。
3、使用 using 展开命名空间中的某变量/函数/结构
此方法为上面两种方法的折中使用:
命名空间中某个变量使用频繁且展开后不会命名冲突,但某个变量不经常使用但有命名冲突,这种情况下可以展开命名空间中常用的变量即可,相当于把某个变量暴露在全局中。
使用方法如下:
此方法不需要写命名空间namespace的关键字。
如下代码所示:
namespace zyb
{
namespace a
{
int i = 10;
}
namespace a
{
struct ii
{
int num1;
double num2;
};
}
}
using zyb::a::i;
int main()
{
i = 20;//可直接使用
struct zyb::a::ii s1 = {s1.num1 = 10, s1.num2 = 1.2f};//还需要域作用限定符进行使用
}
五、C++的输入与输出
C++的输入与输出需要包含头文件<iostream>,是 input output stream 的缩写,是C++标准的输入、输出流库,其中定义了标准的输入、输出对象(C++中的对象可以理解成C中的变量)。
(一)标准输入与输出
① std::cout 标准输出
cout 是ostream的对象,主要面向窄字符(narrow characters(of type char))的标准输出流,作用是把字符以外的类型转化成字符流(字符串),再进行输出。
② std::cin 标准输入
cin 是istream的对象,主要面向窄字符(narrow characters(of type char))的标准输入流,作用是得到字符流后进行解析,转化成对应的整形或浮点数等类型,再输入到内存中。
注意:因为 cin 与 cout 都在C++标准库中,而标准库又在命名空间std中,要使用【域作用限定符】才能对 cin 与 cout 进行使用。
问题:为什么标准输入、输出流要带个c?
回答:因为只有在内存中才存在整形,浮点数等类型;而在其他设备,比如文件、网络、磁盘等中,只支持字符。比如要将整形结果输出到控制台上,就要把整形转化成字符;其他设备上的字符类型数据也要转化后再进入内存进行处理。
(二)流运算符
<< 是流插入运算符,配合cout使用;>> 是流提取运算符,配合cin使用。
std::cout << 的对象可以是任意类型的对象(变量),作用为把右边的对象进行输出到控制台;std::cin >> 的作用是提取流中的字符串,进行转化成对应的类型后进行输入到内存。
C++把C中的左、右移运算符 << 、>> 进行了复用。
使用例如下:
#include<iostream>
int main()
{
int i;
std::cin >> i;
std::cout << i;
return 0;
}
结果如下:
注意:
① << 、>> 支持连续的流插入与流提取,且每次连续的流插入与流提取都可以是不同的类型,因为运算符能够自动识别对象的类型;
② << 流插入的运算过程是从到右依次进行输出,>> 流提取的运算过程是从到右依次进行输入。
如下所示:
int main()
{
int i;
double a;
std::cin >> i >> a;
std::cout << i << a;
return 0;
}
意思是:std::cin 提取字符内容转化后先给变量 i ,然后给变量 a;std::cout 先输出变量 i,然后输出变量 a 到控制台。
(三)换行
有两种换行方法:
方法一:std::cout << i << '\n';
方法二:std::cout << i << std::endl;
std::endl 是一个函数,流插入输出时,相当于插入一个换行字符+刷新缓冲区。
std::endl 后面endl的全程为:end of line。
std::endl的作用可以理解为是C中的换行符 '\n' ,但实际std::endl函数很复杂。
问题:既然可以使用\n进行换行,那为什么还需要使用std::endl呢?
回答:不同类型平台的换行符不一样,可能换行符 '\n' 在另一平台并不适用,而std::endl会适配当前平台的换行符,保证了代码的可移植性。
(四)std::cout 和 std::cin 与 printf 和 scanf 的对比
① printf 和 scanf 需要按格式指定类型,而 << 流插入可以自动识别对象的类型,无需再进行格式化的指定%d,%s等;
他们效果是一样的,都是进行输入与输出,且这四个函数可以混合着用,没有影响;
若想控制输出的小数点或者输出宽度,建议使用printf,这样更加方便。
② cin 与 cout 的效率比 printf 和scanf 要低,因为cin 与 cout 底层是存在缓冲区的,遇到刷新标准才会进行输入输出;且流之间有绑定关系,一个流没遇到刷新标准而后面的流遇到了,系统为了同步两个流,就会调用刷新指标把前一个流推出去,这些调用对系统都有开销,所以效率会低。
③ <iostream>中包含了<stdio.h>,只包含<iostream>也能使用 printf 和 scanf。vs系列的编译器是这样,其他编译器可能会报错。
现在应该能了解C++的第一个程序了。
六、缺省参数
缺省参数是声明或者定义函数时为函数的参数指定一个缺省值。
作用:在调用该函数时,如果没有指定实参,则采用该形参的缺省值;若指定了实参,则会替换掉形参的缺省值,这样可以使函数的调用更加灵活。
缺省参数可以理解为默认参数。
使用示例如下:
void Add(int a = 0, int b = 1)//这里的 0 和 1 就是该函数的缺省值
{
//业务代码
}
int main()
{
Add();
return 0;
}
缺省参数又分为全缺省和班缺省参数:
① 全缺省参数:全部形参都给缺省值;
② 半缺省参数:部分形参给缺省值,C++规定了半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。如下所示:
void Add(int a , int b = 1, int c) 错误赋缺省值
{
//业务代码
}
注意:
① 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。如下所示:
void Add(int a = 5, int b = 1, int c = 7) 错误赋缺省值
{
//业务代码
}
int main()
{
Add( , 8, ); 错误传值给缺省函数
return 0;
}
② 函数声明与函数定义分离时,缺省参数不能再函数声明和定义中同时出现,规定必须函数声明给缺省参数。
③ C语言中无缺省参数。
缺省的实际应用:
若不知道要传什么参数给函数,就用缺省值;若明确要传什么值就直接传参,替换掉缺省值。
总结:
缺省值又叫默认值,若函数不传参就使用缺省值,若传参就使用传递过来的参数替换掉缺省值。
七、函数重载
C++支持在同一作用域中出现同名函数,不过要求这些同名函数的形参不同:参数个数不同,参数类型不同,参数顺序不同(本质还是参数类型不同)。这样C++函数调用就出现了多态行为,使用更加灵活。C语言不支持函数重载。
示例如下:
#include<iostream>
using namespace std;
// 1、参数类型不同
void Add(int a, int b)
{
cout << "int: a + b" << endl;
}
void Add(double a, double b)
{
cout << "double: a + b" << endl;
}
// 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(1, 2);
Add(1.2f, 2.5f);//参数类型不同
f();
f(1);//参数个数不同
f(1, 'a');
f('a', 1);//参数顺序不同
return 0;
}
结果如下:
注意:
① 返回值不同时不是函数重构,因为调用时也无法区分。若返回值相同但类型不同,是属于重构的,如下所示,代码会报错:
② 当同名函数一个没有参数,一个是缺省参数的时候,语法上是构成重载的,但实际用起来还是会报错,因为使用时有歧义,编译器不知道调用谁。如下所示:
八、引用
(一)引用的概念和定义
1、概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如孙悟空的别名叫齐天大圣,猪八戒叫他大师兄,指的都是同一个人物。
2、定义
引用的定义如下:
这里的&其实就是对C语言中的取地址符进行了复用,成为了引用符。
示例代码如下:
#include<iostream>
using namespace std;
int main()
{
int num = 10;
int& i = num;
i = 250;
cout << num << endl;
return 0;
}
结果为:
内存的示例如下:
int main()
{
int a = 0;
int& b = a;
int& c = b;
int& d = c;
return 0;
}
以上代码的内存:
指向的都是同一块空间,本质是同一个空间有多个名称。
3、引用 与 typedef 和 define 的区别
① 引用是给变量取别名,如 :
int& 齐天大圣 = 孙悟空;
② typedef是给类型取别名;
typedef unsigned int uint;
③ define是定义一个宏,预处理时进行替换。
#define a 10;
(二)引用的特性
1、引用在定义时必须初始化
如下所示:
所以不存在空引用,但是有空指针。
2、一个变量可以有多个引用
如下所示:
#include<iostream>
using namespace std;
int main()
{
int num = 10;
int& a = num;
int& b = num;
a = 1000;
cout << b << endl;
return 0;
}
结果为:
3、引用过一个实体后,不能改变指向
如下代码所示:
#include<iostream>
using namespace std;
int main()
{
int num1 = 10;
int& a = num1;
int num2 = 786;
a = num2;
cout << "num1的地址:" << &num1 << endl \
<< "a的地址: " << &a << endl \
<< "num2的地址:" << &num2 << endl;
return 0;
}
在X86环境下的结果为:
此处 a = num2 并不是改变指向,而是赋值。
正是因为不能改变指向,所以引用不能替代指针;指针与引用是相辅相成的关系,而不是替代关系。比如在链表中有三个链接的节点,若删除了中间的节点,第一个节点的指针域就要改变指向,指向第三个节点的数值域;而引用就做不到这一点。若是在函数参数中接收指针地址,就不用创建二级指针了,直接引用指针的别名就可以做到对同一块的使用了,这样会更加简洁易懂。
(三)引用的使用
引用在实际应用中主要干的两件事:【在引用传参和引用做返回值中减少拷贝提高效率】和【改变引用对象时同时改变被引用的对象】。简单来说,就是提高传参和返回值的效率,和能够像指针一样改变同一块地址。
① 引用参数如下:
引用传参
struct arr
{
int a[1000];
};
void Fun1(struct arr& ii)
{
//业务代码;
}
int main()
{
struct arr i = { {0} };
Fun1(i);
return 0;
}
不用再拷贝数组进函数里,直接对原数组进行处理,
极大提高效率
② 引用返回值(前提是返回值出了函数后不能被销毁):
一般的函数返回值是先把返回值临时拷贝到一个临时空间中,两者属于不同的空间,再把临时空间的值作为函数调用的返回值,若直接在调用函数处进行运算就会出错,因为运算是对临时空间中的值进行计算,而不是对真正的返回对象做运算。
若把函数的返回类型设置成【类型&】,这样返回的就是返回值的别名,这样返回值与函数调用处指向的是同一块空间,可以直接对函数的返回值处做运算。这样减少了系统对临时空间开辟的开销,增加了返回的效率。(也可以把返回类型写成指针类型,然后返回值写成地址,这样函数调用处就要解引用,也可以达到目的,但比引用的书写要更麻烦一些)
示例如下所示:
#include<iostream>
using namespace std;
struct Stu
{
int age;
}stu;
int& Fun2(int i)
{
i++;
return i;
}
int main()
{
stu.age = 18;
cout << ++Fun2(stu.age) << endl;
return 0;
}
结果为:
注意:并不是全部情况都能够使用引用返回。比如临时变量的返回使用引用是不合法的,会越界访问。(使用指针返回也是野指针)
函数的形参也可以是引用参数,这样形参与实参指向的是同一块空间,这样的传值类似于传址调用(指针传参)。
如下:
改变引用对象时同时改变被引用的对象
#include<iostream>
using namespace std;
void Add(int& a)
{
a = a + 3080;
}
int main()
{
int i = 10;
Add(i);
cout << i << endl;
return 0;
}
直接对同一块空间做处理
结果为:
注意:
① 能对指针变量进行引用,这样对函数传指针就不许要二级指针了,相对而言简化了程序。
如下所示:
int main()
{
int i = 10;
int* p = &i;
int*& ip = p;
return 0;
}
② 与指针的区别:引用不能改变指向,不能完全替代指针。
(四)const引用
1、const引用与const对象
可以引用一个const对象,但是必须用const引用,否则会报错:
因为变量被const修饰后的访问权限会变小,若直接引用被const修饰的变量,就属于访问权限放大,这是禁止的;可以使用const引用来引用普通对象,这样是属于把普通对象的访问权限缩小,是可以的。(对象的访问权限在引用过程中可以缩小,但不能放大,相当于只能向下兼容)
形象理解:对象就是老大,引用就是小弟,老大下了规定而小弟就不能做规定外的事情;而老大没有规定,那么小弟给自己规定是没问题的。
如下所示:
这种情况下 i 能修改, 而 num 不能修改,因为 num 的权限不会影响 i 。
注意:只有引用和指针才存在访问权限的放大和缩小问题。
此处 *p 是不能被修改的,若赋值给指针 a ,那么 *a 就能修改 i,这属于访问权限的放大。
2、临时对象触发权限放大
需要注意的是类似 【int& rb = a*3; 】【double d = 12.34; 】【int& rd = d;】 这样⼀些场景下a*3表达式的结果保存在一个临时对象中,【int& rd = d】也是类似,在类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性(常量的性质,与被const修饰一样),所以这里就触发了权限放大,必须要用const引用才可以。
所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。
总结:【表达式】与【类型转化】的接收会创建临时对象,而临时对象具有常量的性质,会缩小访问权限,要使用const引用。
3、const引用的使用
通常使用于函数的参数接收:
void Fun(const int& i);
int main()
{
Fun(10);
return 0;
}
这样就会保证参数的接收的范围变大(包括:① const对象, ② 普通对象, ③ 临时对象),且不会扩大访问权限。
(五)指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
• 语法概念上引用是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引用⼀个对象后,就不能再引用其他对象;而指针可以不断地改变指向对象。
• 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
• sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。(32位平台下占4个字节,64位下是8个字节)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
注意:
删除空指针是无害的,因为指针变量与指向的变量是两个空间;
而删除引用会导致原空间也会被删除。
九、inline 内联函数
用 inline 修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以减少开销提高效率。
如下所示:
inline int Add(int a, int b)
{
return a + b;
}
int main()
{
int num1 = 5;
int num2 = 6;
Add(num1, num2);
return 0;
}
inline 对于编译器而言只是一个建议,也就是说,你加了 inline 编译器也可以选择在调用的地方不展开,不同编译器关于 inline 什么情况展开各不相同,因为C++标准没有规定这个。inline 适用于频繁调用的短小函数(八九行左右),对于递归函数,代码相对多一些的函数,加上 inline 也会被编译器忽略。
宏中有很多的坑:多用括号解决优先级问题和结尾不要写分号多增加一个空语句,但是宏的替换机制可以使调用时不用建立栈帧,提高效率。
而 inline 就是解决宏的问题而设计出来的,既没有宏的坑,也会提高效率。唯一的缺点就是 inline 对于编译器来说是个建议,无法做到百分比替换。
vs 编译器 debug 版本下⾯默认是不展开 inline 的,这样方便调试,debug 版本想展开需要设置⼀下以下两个地方。
展开后的底层汇编代码如下,没有 call 指令,已展开代码:
int num1 = 5;
00007FF73A1C1562 mov dword ptr [num1],5
int num2 = 6;
00007FF73A1C156A mov dword ptr [num2],6
Add(num1, num2);
return 0;
00007FF73A1C1572 xor eax,eax
判断是否展开:
函数被编译后是一堆指令,需要储存起来执行。第一句指令的地址就是函数的地址。(数字后面的 h 是16进制的后缀)
汇编指令中,只要有 call 指令,就是建立函数栈帧了,就不展开。
注意:
① 为什么代码过长就不展开了:因为代码过长展开后会使可执行文件(.exe)膨胀,加载进计算内存的进程中也会使进程膨胀,导致执行缓慢等问题。所以展开的决策权交给程序来判断。
② inline 不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,在底层汇编无法使用 call 指令,链接时会出现报错。
解决:声明和定义写一起。
③ 可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数,因为 inline 函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突。
十、nullptr
NULL实际是一个宏,在传统的C头文件( stddef.h )中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
在c++中,NULL被定义成 0 ,而在c中,被定义成地址为0的泛型指针。
在如下代码中就会出错:
#include<iostream>
using namespace std;
void f1(int i)
{
cout << "f1(int i)" << endl;
}
void f1(int* ptr)
{
cout << "f1(int* ptr)" << endl;
}
int main()
{
f1(0);
f1(NULL);
return 0;
}
结果如下:
都调用第一个代码了,C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过 f1(NULL) 调用指针版本的 f1( int* ptr) 函数,但是由于NULL被定义成0,因此调用了 f1(int i) 函数,因而与程序的初衷相悖,若使用 f1((void*)NULL),则调用会报错。
如下所示:
为了解决这个问题。C++11中引入 nullptr,nullptr 是一个特殊的关键字,是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型(自动识别)。使用 nullptr 定义空指针可以避免类型转换的问题,因为 nullptr 只能被隐式地转换为指针类型,因而不能被转换为整数类型。
#include<iostream>
using namespace std;
void f1(int i)
{
cout << "f1(int i)" << endl;
}
void f1(int* ptr)
{
cout << "f1(int* ptr)" << endl;
}
int main()
{
f1(0);
f1(nullptr);
return 0;
}
结果为:
在C++中空指针就用 nullptr 进行接收
十一、有意思的点
(一)越界访问不一定报错
越界读,不报错。
int main()
{
int arr[10] = { 0 };
cout << arr[11] << endl;
cout << arr[12] << endl;
cout << arr[13] << endl;
return 0;
}
结果为:
越界写不一定报错,一般是抽查。
如下演示:
int main()
{
int arr[10] = { 0 };
arr[10] = 1;//报错
arr[11] = 1;//报错
arr[12] = 1;//不报错
return 0;
}
因为vs数组的后两位为抽查位,过了抽查位再进行修改就不会报错;不同平台,不同编译器的结果不同。
(二)引用与指针的底层
演示代码如下:
int main()
{
int i = 10;
int& num = i;
num++;
int* p = &i;
*p++;
return 0;
}
部分反汇编代码如下:
int i = 10;
00007FF6F4091DEE mov dword ptr [i],0Ah
int& num = i;
00007FF6F4091DF5 lea rax,[i]
00007FF6F4091DF9 mov qword ptr [num],rax
num++;
00007FF6F4091DFD mov rax,qword ptr [num]
00007FF6F4091E01 mov eax,dword ptr [rax]
00007FF6F4091E03 inc eax
00007FF6F4091E05 mov rcx,qword ptr [num]
00007FF6F4091E09 mov dword ptr [rcx],eax
int* p = &i;
00007FF6F4091E0B lea rax,[i]
00007FF6F4091E0F mov qword ptr [p],rax
*p++;
00007FF6F4091E13 mov rax,qword ptr [p]
00007FF6F4091E17 add rax,4
00007FF6F4091E1B mov qword ptr [p],rax
return 0;
00007FF6F4091E1F xor eax,eax
mov汇编指令为移动;lea汇编指令为取地址,[ ] 为取地址。
可以看到,在语法层面上引用不开空间,而指针开辟空间;但是在底层汇编层面,是没有引用概念的,引用也是用指针实现的,几乎相同。