=========================================================================
相关代码gitee自取:
C语言学习日记: 加油努力 (gitee.***)
=========================================================================
接上期:
【c++初阶】一、入门知识讲解
(C++关键字、命名空间、C++输入&输出、缺省参数、函数重载)-CSDN博客
=========================================================================
六 . 引用
(1). 引用的概念和特性:
引用的概念:
在C语言中,我们可以通过 指针 来实现对一个已存在的变量的“调用”,
但指针可能相对会比较难理解一些,
而在C++中,我们可以通过 引用 来实现对一个已存在的变量的“调用”,
通过一个已存在的变量(引用实体)可以设置一个 引用变量 ,
引用变量不是新定义一个变量,而是给已存在变量(引用实体)取了一个别名,编译器不会为引用变量开辟内存空间,
引用变量和被它引用的变量(引用实体)共用一块内存空间。
(如:孙悟空,又叫弼马温,又叫齐天大圣,本质还是同一个人)
生成引用变量的方式:
- 引用变量类型& 引用变量名(对象名) = 引用实体
---------------------------------------------------------------------------------------------
引用的特性:
- 引用变量的类型必须和引用实体是同种类型的
- 引用变量在定义时必须初始化
- 一个变量可以有多个引用
(如:孙悟空被称为齐天大圣 ,而齐天大圣又可以被称为大圣)
- 一个引用变量一旦引用了一个实体(引用实体),
就不能再引用其它实体(引用实体),所以引用无法代替指针
- 因为引用变量和引用实体共用同块内存空间,
所以引用变量的改变也会导致实体变量的改变图示:
(2). 引用的使用场景:
(重点)引用变量做函数的参数:
- 将函数的参数设置为引用变量,主函数调用函数时直接传变量名即可,
变量名传到函数这边后,函数中使用的就是该变量(引用实体)的引用变量,
又因为引用变量和引用实体共用同一块内存空间,
所以函数中引用变量(形参)的改变,也会导致引用实体(实参)的改变,
形参是实参的“别名”(引用变量),形参的改变就改变了实参
图示:
- 传引用做参数的效率比传值做参的效率高
图示 -- 传值做参和传引用做参的效率对比:
---------------------------------------------------------------------------------------------
(重点)引用变量做返回值:
- 将函数的返回值设置为引用变量,函数执行完成后,直接返回对应变量即可,
编译器在返回时会自动生成该返回变量的引用变量并返回(名字由编译器决定),
主函数中也要通过对应类型的引用变量来接收该函数返回的引用变量,
需要注意的是,函数中被返回的变量应该是静态变量,
这样可以保证该变量在函数结束后不会被销毁,引用变量作为该变量的“别名”,
共同使用同一块内存空间,该空间不会被销毁才能保证引用变量的值是准确的
- 还有一点,如果被返回的变量不是静态变量,
主函数第一次调用该函数并用对应引用变量接收时,其接收的值是不确定的,
接收结果取决于编译器在函数调用后会不会销毁对应栈帧,
如果会销毁,则主函数中引用变量接收的值为随机值,
如果不会销毁,则接收的值为函数执行后的正常返回值,
(注:VS编译器在函数调用结束后不会销毁对应栈帧)
同时因为主函数中的引用变量和调用函数中的返回对象共用同一块内存空间,
所以在之后再次调用该函数且不接收其返回值的情况下,
主函数中的引用变量,即调用函数的返回对象的“别名”还会被改变,
(不接收函数返回值但“别名”还是被改变了,这肯定不合适)
所以函数中被返回的变量应该是静态变量正确使用例子:
错误使用例子:
- 传引用返回的效率比传值返回的效率高
图示 -- 传值返回和传引用返回的效率对比:
- 传引用返回可以修改返回对象
图示:
(3). 常引用:
- 一个变量的权限分为可读可写、只读、只写,
一般创建一个变量后,该变量的权限为可读可写,
但如果对其进行const修饰附加常属性后,其权限就变成了只读,
即权限从可读可写变成了只读,该变量的权限缩小了,
而此时如果要对该变量进行引用,那么引用变量也需要被const修饰,
const引用变量 引用 const变量,成为const变量的“别名”,实现了权限的平移
引用变量实现权限的平移是被允许的,而引用变量实现权限的缩小也是被允许的,
即引用实体的权限是可读可写,而引用变量的权限是只读
引用变量实现权限的平移和缩小都是被允许的,唯独对权限的放大是不允许的,
即引用实体的权限是只读(被const修饰),
而引用变量的权限却是可读可写(未被const修饰)
- 补充:
当需要进行类型转换时:强制类型转换、隐式类型转换、类型截断,
它都不是将右值转换为另一个类型然后就直接赋给左值的,
而是会生成一个临时变量,
先将转换后的右值赋给临时变量,再将此时的临时变量赋给左值。
所以当你进行引用操作时,如果涉及到了类型转换,
引用实体会是类型转换过程中的临时变量,即给临时变量起了个“别名”,而不是对左值图示:
(4). 引用和指针的区别:
- 在语法层面(概念层面)上:
引用是定义一个变量的“别名”,而指针是存储一个变量的地址
- 引用在定义时必须初始化,而指针则没有要求
- 引用在初始化引用一个实体后,就不能再引用其它实体了,
而指针则可以在任何时候指向任何一个同类型实体
- 没有NULL引用(空引用),但有NULL指针(空指针)
- 引用和指针在sizeof中含义不同:
引用的结果为引用类型的大小,
而指针的结果始终是地址空间所占字节个数(32位平台下为4个字节)
- 引用自加(++)即引用的实体增加1,
而指针自加(++)即指针向后偏移一个类型的大小
- 没有多级引用,但有多级指针
- 引用和指针访问实体的方式不同:
引用的话编译器会自己处理,而指针则需要显示解引用(*)
- 引用比指针使用起来相对更安全(因为引用只能指定一个实体)
补充 -- 引用的底层实现:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
七 . 内联函数
(1). 内联函数的概念:
- 以inline修饰的函数叫做内联函数,
编译时C++编译器会在调用函数的地方展开该内联函数
- 内联函数没有函数调用建立栈帧的开销,所以内联函数可以提升程序运行的效率
- 内联函数克服了宏函数的缺点,
不用在意返回时括号的设置,还可以进行调试,
同时还有宏函数的优点,不用建立栈帧,
直接在调用函数的位置就会展开内联函数,提高效率现有一个宏函数:
使用内联函数替代宏函数:
(2). 内联函数的特性:
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,
在编译阶段,会用函数体替换函数调用
缺点:可能会使目标文件变大 ;优势:少了调用开销,提高程序运行效率
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,
一般建议:
对函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、
不是递归、调用频繁的函数采用inline修饰,
否则编译器会忽略inline特性,还是通过call指令通过函数地址调用函数
- inline不建议声明和定义分离,分离会导致链接错误,
因为内联函数的声明在头文件中被展开,会导致没有函数地址,
在链接时call指令就找不到函数实现(定义)的地址了图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
八 . auto关键字(C++11)
(1). auto简介:
在早期C/C++中auto的含义是:使用auto修饰的变量,是自动存储器的局部变量。
在C++11中,标准委员会赋予了auto全新的含义:
auto不再是一个存储类指示符,而是作为一个新的类型指示符来指示编译器,
auto声明的变量由编译器在编译过程中推导得到,
定义一个auto类型的变量并对其进行初始化后,auto变量的类型可以被自动推导出来
(2). auto的使用和使用注意事项:
auto的使用:
auto变量可以自动推导类型(初始化后),
auto变量的初始化对象可以是普通变量、指针变量和引用变量,auto变量除了有推导变量类型的作用,还可以起到省略类型的定义的作用,
因为C++中有些变量的类型是很复杂的,在理解了类型的情况下可以使用auto进行省略
图示 -- auto变量推导变量类型(不常用):
图示 -- auto变量省略类型的定义(常用):
---------------------------------------------------------------------------------------------
auto的使用注意事项:
- auto与指针、引用的结合使用:
用auto声明指针类型时,用auto或auto*都可以没有区别;
但用auto声明引用类型时则必须加&,即auto&
- 在同一行定义多个auto变量:
当在同一行声明多个auto变量时,这些auto变量的初始化变量必须是相同的类型,
否则编译器将会报错,因为编译器实际只对第一个初始化变量类型进行推导,
然后用推导出来的类型定义其它的初始化变量
- auto不能作为函数的参数(最好也不要做返回值);auto不能直接用来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常用的优势用法是在C++11中提供的新式for循环中,
还有lambda表达式等进行配合使用
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
九 . 基于范围的for循环(C++11)
(1). 范围for循环的语法:
在C语言中,对数组的遍历操作可以通过数组下标或者指针的形式。
但对于一个本身就有范围的集合来说,还要程序员来说明循环的范围是有些多余的,
还有可能因此造成错误。因此C++11中引入了基于范围的for循环,
范围for循环后的括号通过冒号“ : ”分为两部分:第一部分为在范围内用于迭代的变量;第二部分则表示被迭代的范围
即:for(在范围内用于迭代的变量 : 被迭代的范围)
注:
与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
图示:
(2). 范围for循环的使用条件:
- for循环迭代的范围必须是确定的:
对于数组而言,其范围就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围错误示范:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
十 . 指针空值nullptr(C++11)
(1). C++98中的指针空值:
- 在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,
否则可能会出现不可预料的错误
- 比如未初始化的指针,如果一个指针没有合法的指向,
我们一般会这样对其进行初始化:int* ptr1 = NULL;
这里的NULL实际是一个宏,在传统的C语言头文件<stddef.h>中,
NULL可能被定义为字面常量0,或者被定义为无类型指针 (void*) 的常量0
- 在使用空值的指针时,都不可避免地会遇到一些麻烦,
当你想把NULL定义为无类型指针 (void*) 的常量时,NULL可能被编译器定义为常量0,
因此与程序的初衷相悖
- 在C++98版本中,字面常量0既可以是一个整型数字,
也可以是无类型的指针(void*)常量,但是编译器默认情况下会将其看成是一个整型常量,
如果要将其按照指针方式来使用,还必须对其进行强转为 (void*)0,
显得头文件中的定义有些多余
- 所以在C++11版本中,有了一个新关键字:nullptr,来专门表示指针空值,
使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是一个关键字
- 在C++11中,sizeof(nullptr) 与 sizeof((void*) 0) 所占的字节数相同
- 为了提高代码的健全性,在未来的编程中表示指针空值时建议最好使用nullptr
图示:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
C++入门知识讲解 -- 对应代码:
Test.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> /* * //C++ 兼容 C * //C语言版“hello world”: * int main() * { * printf("hello world\n"); * * return 0; * } */ //CPP: //兼容C语言 //面向对象 //泛型 //C的不足的弥补 /* //C++版“hello world”: #include <iostream> //第一个特性:命名空间 using namespace std; int main() { //第二个特性:IO流 cout << "hello world" << endl; return 0; } */ //用C语言解释CPP的命名空间: /* * C语言不足:命名冲突 * 1、我们写的变量名和库中的函数冲突 * 2、两个程序员定义了相同的变量名导致冲突 */ //int rand = 0; rand变量 和 <stdlib.h>头文件 中的rand()函数冲突了 // 所以可以使用namespace命名空间解决该问题: //namespace my //{ // //命名空间中定义属于自己的rand变量: // int rand = 0; // // //命名空间中还可以再嵌套子命名空间: // namespace my1 // { // int rand = 1; // } // // //命名空间中定义属于自己的Add函数: // int Add(int left, int right) // { // return left + right; // } // // //命名空间中定义属于自己的Node结构体: // //(C++中叫做类) // struct Node // { // struct Node* next; // int val; // }; //} 使用命名空间时类似结构体,但{}后不用加; // //namespace your //{ // int rand = 0; //} my命名空间中的rand和 your命名空间中的rand独立 // // //int main() //{ // printf("hello world\n"); // // printf("%p\n", rand); // //使用namespace命名空间后, // //这里默认调用的就是全局中的rand // //即<stdlib.h>头文件中的rand函数(指针) // //而不会找命名空间中的rand // // //如果要使用my命名空间中的rand变量 // //需要使用到 // // :: -- 域作用限定符 // printf("%d\n", my::rand); // // //通过域作用限定符来使用 // //my命名空间中的Add函数: // my::Add(1, 2); // // //通过域作用限定符来使用 // //my命名空间中的Node结构体(类): // struct my::Node node; // //注:命名空间是指定在结构体名称的位置 // // 而不是指定在struct的前面 // // //访问嵌套的子命名空间my1: // printf("%d\n", my::my1::rand); // // return 0; //} #include <stdio.h> #include <stdlib.h> /* * //全局变量:rand * int rand = 0; */ namespace ggdpz //俺滴博客名 { //命名空间中可以定义变量、、函数、类型…… int rand = 10; //变量 int Add(int left, int right) //函数 { return left + right; } struct Node //类型(结构体) { struct Node* next; int val; }; //命名空间中嵌套子命名空间: namespace ggdpz1 { int rand = 20; } } //同一程序中的同名命名空间 namespace ggdpz { int rand1 = 30; } 使用方式二对命名空间进行展开: using namespace ggdpz; // 使用方式三对命名空间的某个成员进行展开: //using ggdpz::rand1; // //int main() //{ // //printf("%d\n",rand); // //rand变量 和 <stdlib.h>头文件 // //中的rand()函数冲突了 // // /* // * 编译后报错: // * error C2365:“rand”:重定义:以前的定义是“函数” // * // * C语言没有办法解决类似这样的命名冲突问题, // * 所以C++中提出了namespace命名空间来解决该问题 // */ // // //通过方式一使用命名空间内容: // //printf("通过方式一:%d\n", ggdpz::ggdpz1::rand); // // //通过方式二使用命名空间内容: // //printf("通过方式二:%d\n", rand1); // // //通过方式三使用命名空间内容: // printf("通过方式三:%d\n", rand1); // // printf("未通过方式三:%d\n", ggdpz::rand); // // // return 0; //} // //#include "Stack.h" // 展开命名空间(更加方便自己测试): //using namespace ggdpz; 展开后就默认使用该命名空间中的内容 // ///* //* using namespace std; //* //std:C++官方库定义的命名空间 //* //展开后库里的东西就可以顺便用了 //* //工程项目中不要展开std,容易命名冲突 //* //如果是日常练习,为了方便可以展开 //*/ // //int main() //{ // //获取命名空间中的栈类型: // //ggdpz::ST s; //展开命名空间前 // ST s; //展开命名空间后 // //先到全局中找ST,再到第一个展开的命名空间中找ST // //然后到第二个展开的命名空间中找ST…… // //找到了就停止(“从上到下找”),未找到则报错 // // //获取命名空间中的栈初始化函数: // //ggdpz::StackInit(&s); //展开命名空间前 // StackInit(&s); //展开命名空间后 // // //获取命名空间中的出栈函数: // //(展开命名空间前) // //ggdpz::StackPush(&s,1); // //ggdpz::StackPush(&s,2); // //ggdpz::StackPush(&s,3); // // //获取命名空间中的出栈函数: // //(展开命名空间后) // StackPush(&s, 1); // StackPush(&s, 2); // StackPush(&s, 3); // // return 0; //} 方式一:指定命名空间 方式二:展开命名空间(谨慎使用) // //#include <iostream> C++经常会包含一个头文件: <iostream> IO流 C++中的头文件一般不写"…….h"(比较老的编译器才写) // ///* //* 每次指定命名空间很不方便 //* 直接展开命名空间全部暴露,又有冲突风险 //* 这时使用 指定展开 就可以解决问题: //*/ //using std::cout; //指定展开std中的cout对象 //using std::endl; //指定展开std中的endl对象 // //int main() //{ // std::cout << "hello world"; // // << -- 流插入运算符 (C++) // //在C语言中是位运算符, // //在C++中多了个身份:流插入运算符 // // //cout -- console out -- 控制台输出 -- 流插入 // //console可以理解为控制台/终端/黑窗口(是一个对象) // //是std库中的内容 // // //所以 std::cout << "hello world"; // //其实可以理解为将“hello world”这个字符串 // //流进std::cout这个终端中 // // int a = 10; // double b = 11.11; // // //使用cout这个对象可以自动识别变量的类型 // //即不用像C语言中打印时需要加上%d、%p…… // //直接让该变量流进cout对象即可: // // std::cout << a; // std::cout << b; // //自动识别输出变量的类型 // // //换行: // std::cout << "helloc world\n"; // // std::cout << a << "\n"; //"\n" -- 字符串换行 // std::cout << a << '\n'; //'\n' -- 字符换行 // std::cout << b << "\n"; // std::cout << b << '\n'; // // //也可以合并写成: // std::cout << a << "\n" << b << '\n'; // // //C++中通常将换行符"\n"写成std::endl // //endl -> endline // std::cout << a << std::endl << b << std::endl; // // //指定展开后可写为,又不会有命名冲突风险: // cout << "hello world"; // cout << a; // cout << b; // cout << "helloc world\n"; // cout << a << "\n"; //"\n" -- 字符串换行 // cout << a << '\n'; //'\n' -- 字符换行 // cout << b << "\n"; // cout << b << '\n'; // cout << a << "\n" << b << '\n'; // cout << a << std::endl << b << std::endl; // // //cin -- console in -- 控制台输入 -- 流提取 // //console可以理解为控制台/终端/黑窗口(是一个对象) // //是std库中的内容 // std::cin >> a >> b; // //让你在控制台上输入的数据分别流入a和b这两个变量中 // // // >> -- 流提取运算符 (C++) // //在C语言中是位运算符, // //在C++中多了个身份:流提取运算符 // // return 0; //} 包含IO流头文件: //#include <iostream> // 指定展开命名空间成员: //using std::cout; //指定展开标准输出对象(控制台) //using std::cin; //指定展开标准输入对象(键盘) //using std::endl; //指定展开C++换行符号 // //int main() //{ // int a = 10; //整型变量 // double b = 3.14; //浮点型变量 // // cout << "使用cout打印当前a和b:" << endl; // // //使用cout进行输出: // cout << a << endl << b << endl; // /* // * 通过cout标准输出对象和<<流插入运算符进行输出打印: // * // * 先将a这个变量流进std::cout这个控制台中打印, // * 再进行endl换行,再将b这个变量 // * 流进std::cout这个控制台中打印,再换行。 // * // * 即使 a变量 和 b变量 的类型不同也能打印 // * C++的输入和输出可以自动识别变量类型 // */ // // cout << "使用cin分别输入数据到a和b:" << endl; // // //使用cin进行输入: // cin >> a >> b; // /* // * 通过cin标准输入对象和>>流提取运算符对数据进行输入: // * // * 让你在控制台上输入的数据分别流入a和b这两个变量中 // * // * 即使 a变量 和 b变量 的类型不同也能输入 // * C++的输入和输出可以自动识别变量类型 // */ // // cout << "输入后再使用cout进行输出打印:" << endl; // // cout << a << endl << b << endl; // // return 0; //} /* //函数的 缺省参数(默认参数) : void Func(int a = 0) //这里的 “=0” 就是缺省参数(默认参数) //(如果没有接收对应的参数,则缺省参数就是该参数的值) { cout << a << endl; } int main() { Func(); //不传参数的话,0就是参数a的默认值 Func(10); //接收参数的话,10(该参数)就是a的默认值 return 0; } */ 缺省参数: // 包含IO流头文件: //#include <iostream> // 展开std命名空间(为了方便) //using namespace std; // 全缺省参数: //void Func(int a = 10, int b = 20, int c = 30) //{ // cout << "a = " << a << endl; //打印a(c++) // cout << "b = " << b << endl; //打印b(c++) // cout << "c = " << c << endl; //打印c(c++) //} // // // 半缺省参数: ///* //* 半缺省的情况下, //* 到了要设置缺省参数时, //* 不能“中断着给”,只能“连续给”, //* 即:半缺省只能“从右往左给”,必须连续给或者只给一个 //* //* 例如: //* 假设有三个参数,给缺省参数时不能是(给,不给,给) //* 但可以“从左往右连续给”,即(不给,给,给), //* 或只给一个: //*(给,不给,不给)、(不给,给,不给)(不给,不给,给) //* 注: //* 不能是“从左往右连续给”,因为如果是(给,给,不给) //* 然后调用函数时是:Fun(1,2),这时就会有歧义, //* 不知道这两个参数是给该函数的哪个参数的, //* 而如果是“从右往左连续给”,Fun(1,2)中: //* 1就对应第一个参数,2就对应第二个参数 //* //* 缺省参数不能在 “声明” 和 “实现” 中都给, //* 都给的话,如果“声明”和“实现”中给的缺省参数不一样, //* 编译器就不知道要用哪个缺省参数, //* 如果只在当前文件的“实现”中给缺省参数的话, //* 到时候又有个新文件调用“实现”, //* 新文件中的“实现”又需要重新给缺省参数, //* 所以最好还是只在“声明”中给到函数缺省参数 //*/ //void Func(int a, int b = 20, int c = 30) //{ // cout << "a = " << a << endl; //打印a(c++) // cout << "b = " << b << endl; //打印b(c++) // cout << "c = " << c << endl; //打印c(c++) //} // 主函数: //int main() //{ // //有了缺省参数后, // //可以调整参数的各种形式来调用该函数: // Func(); // Func(1); // Func(1, 2); // Func(1, 2, 3); // // //Func(, 2, ); //不能“跳着(间隔)”设置缺省参数 // // return 0; //} // // // // // 函数重载: 函数名相同,但参数情况不同(类型不同、个数不同) 注:类型不同又包括顺序不同 // 参数情况不同:类型不同 //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 = 0) //{ // cout << "f(int a)" << endl; //} ///* //* 结合后能够构成重载, //* 但这时如果不传参数的话就会存在二义性(歧义), //* 不知道调用f()函数还是f(int a = 0)函数 //*/ // // //int main() //{ // //调用整型相加的Add函数: // Add(1, 2); // //调用浮点型相加的Add函数: // Add(1.1, 2.2); // // Add(1, 2.2); // /* // * 已经定义了一个整型的Add()函数, // * 还有一个浮点型的Add()函数了, // * 这时如果使用Add()函数,且一个参数为整数,另一个参数为浮点数, // * 这时会报错,如果整型和浮点型Add()函数我们只实现了其中一个, // * 其实是可以可以通过的, // * 因为可以将其中一个参数的类型隐式转换为另一种类型, // * 而我们即定义了整型Add()函数又定义了浮点型Add()函数, // * 就不清楚要隐式转换为何种类型了(有歧义) // */ // // //调用参数个数为0的f函数: // f(); // //调用参数个数为1的f函数: // f(1); // // // //系统会根据你参数的情况来匹配对应的重名函数 // // return 0; //} //包含Func函数头文件: //#include "Func.h" // //int main() //{ // //分别调用重载函数: // Func(1, 'x'); // Func('y', 2); // // return 0; //} /* * 为什么C语言不支持函数重载,而C++支持函数重载?? * * 需要先复习一下编译链接过程: * * 有三个文件: * Func.h Func.cpp Test.cpp * * 执行时需要进行以下过程: * 预处理 -> 编译 -> 汇编 -> 链接 * * 一: * 预处理:头文件展开(主) / 宏替换 / 条件编译 / 去除注释 * 在Func.cpp文件和Test.cpp文件中,因为包含了Func.h头文件, * 在预处理时会对头文件进行展开,之后会生成预处理文件:Func.i, * 在该文件中就会有Func函数的声明和实现(Func.i:函数声明和定义) * 而Test.cpp文件同理会生成一个Test.i预处理文件, * 该文件包含函数的声明和调用(Test.i:函数的声明和实际调用) * * 二: * 编译:检查语法 / 生成汇编代码 * 编译时会生成汇编代码文件(.s文件), * 即 Func.s文件 和 Test.s 文件, * (分别由 Func.i 和 Test.i 生成) * Func.s文件中存放了重载函数两个f()函数对应的汇编代码, * 而Test.s文件中则存放了主(main)函数的汇编代码, * 包括其调用的两个f()函数(重载函数)的汇编代码, * 而要调用这两个函数,需要用到汇编语言中的call指令来获取函数地址, * 但是在编译阶段,因为Test.cpp中只包含了Func.h头文件,只有函数声明, * 没有函数实现(定义),所以call指令还无法获得对应的函数地址, * 而编译器会判断调用的函数和头文件中函数是否匹配,匹配的话, * 即使call指令还没找到函数地址,也可以先让其通过(为了实现多文件项目) * * 三: * 汇编:将汇编代码文件中的代码转换为二进制的机器码(CPU能“读懂”的代码) * 汇编后会生成 Func.o 和 Test.o 目标文件, * (分别由 Func.s 和 Test.s 生成) * * 四: * 链接:将目标文件合并到一起,链接一些没有确定的函数地址等等 * 将汇编中的 Func.o 和 Test.o 文件合并为 a.out 文件(默认情况下) * 合并后的a.out文件中: Test.o 中之前call函数未找到的函数地址, * 就能在 Func.o 中的函数实现(定义) 中找到了, */ /* * 所以我们可以知道,在“编译”时call指令未找到调用函数地址, * 等到了“链接”时合并文件后才能找到。 * * 在C语言中,没有重载函数,即函数名唯一的情况下, * 要找函数地址只需要通过唯一的函数名即可找到 * (在“链接”步骤中通过函数名在.o目标文件的符号表进行查找地址) * 所以C语言中如果有重载函数,函数名不唯一就无法“链接”到函数地址 * * 而在C++有重载函数,即函数名不唯一的情况下,(不同环境实现方式不同) * (Linux的g++中)C++有一个函数,可以通过函数名和参数情况修饰出一个名字, * 函数名相同但参数情况不同,修饰出的名字不同, * 再通过修饰出的名字来查找对应的函数地址。 * (修饰名字构成:_Z + 函数名字符个数 + 函数名 + 各参数首字母) * (如:Func(int a, double b) 修饰后名字为:_Z4Funcid) */ /* * 引用:给已存在的变量取一个别名 * 编译器不会为引用变量开辟内存空间, * 它和它引用的变量共用同一块内存空间 */ 包含IO流头文件并展开std命名空间: //#include <iostream> //using namespace std; // //int main() //{ // //创建一个变量a(假设a为“孙悟空”): // int a = 1; // // //正常情况可以创建另一个变量来存储a的值: // //(通过指针表示也可以) // int b = a; // // //使用引用变量表示a(“孙悟空又叫弼马温 -- c”): // int& c = a; // /* // * 相当于给已存在的a变量取了一个别名, // * 引用变量c和变量a共用同一块内存空间 // * (a就是c,c就是a,只是叫法不同) // */ // // b++; //变量b不和变量a共用内存空间 // //所以b++不会影响到变量a // // c--; //引用变量c和变量a共用内存空间 // //所以c--会影响到变量a的值,a也会-- // // //取了一个"外号"后,即有了一个引用变量后 // //还可以再取多个"外号",即可以取多个引用变量: // //(“孙悟空除了叫弼马温,还叫齐天大圣 -- d”) // int& d = a; // // //也可以对“外号”再取一个“外号”, // // (“齐天大圣又可以简称大圣 -- e”) // //即对引用变量也可以设置一个引用变量: // int& e = c; // // /* // * a-孙悟空 c-弼马温 d-齐天大圣 e-大圣 // * c、d、e都是引用变量,都是对变量a的引用 // * 所以a、c、d、e都共用同一块内存空间 // * 即这4个变量的地址都相同: // */ // //打印这四个函数的地址: // cout << "变量a的地址:" << &a << endl; // cout << "其引用变量c的地址:" << &c << endl; // cout << "其引用变量d的地址:" << &d << endl; // cout << "其引用变量e的地址:" << &e << endl; // // return 0; //} //引用的意义之一:做参数 C语言:指针 //void Swap(int* left, int* right) //{ // int temp = *left; // *left = *right; // *right = temp; //} //两值交换函数(通过指针实现) // C++:引用 定义另一个Swap函数,构成函数重载: //void Swap(int& left, int& right) //{ // int temp = left; // left = right; // right = temp; //} //两值交换函数(通过引用变量实现) //int main() //{ // int a = 1; // int b = 2; // // //通过函数调换两值: // Swap(&a, &b); //传地址实现交换 // // Swap(a, b); //正常传变量实现交换 // /* // * 因为重载函数Swap(int& left, int& right), // * 参数 left 和 right 都是引用变量, // * 主函数正常传递变量a和b来调用该函数, // * 该Swap函数接收后: // * int& left 就是 int a 的 引用变量; // * int& right 就是 int b 的 引用变量, // * 所以 left 和 a 共用同一地址,right 和 b 共用同一地址, // * 这样一来,形参left和right的改变就能分别改变实参a和b, // * 所以不会导致在调用函数时形参改变而实参不改变的情况 // */ // // return 0; //} /* * C++中 引用 可以替代 指针 吗? * 答案是不行,C++的引用相较java的比较特殊, * java中的引用可以改变指向,而C++的不行, * 如: * b是a的引用变量(“别称”),还有个c变量, * Java中就可以将引用变量b改变指向c变量,成为c变量的“别称” * 而C++中就不行了,b只能是a的引用变量(“别称”) * * C++中,在数据结构的链表中就无法用引用来代替指针, * 当要插入一个结点时,需要改变next指针的指向, * 而不能用引用来实现相同操作,因为引用无法改变指向 */ //int main() //{ // //定义一个变量a作为引用实体: // int a = 0; // // //1:进行“引用”,“引用”时必须初始化, // //因为要“起别名”,首先得有引用实体,指明对谁进行“引用” // int& b; // // //引用 变量a “生成” 引用变量c: // int& c = a; // // //再生成一个普通变量d: // int d = 1; // // //2:C++的引用无法改变指向: // //如果此时将普通变量d赋给a的“别名”c: // c = d; // //只会将变量d的值赋给a的“别名”c, // //二不会改变“别名”(引用变量)的指向, // //c还是a的“别名”,不会变成d的“别名” // // //3:一个对象可以有多个“别名”,可以为“别名”再取一个“别名”: // int& e = a; //a现在有c和e两个“别名” // int& f = e; //“别名”e现在又有一个“别名”f // // return 0; //} /* * phead是结点的地址,*phead可以改变结点 * 但尾插可能会改变phead指针本身, * 这时就要定义phead的二级指针pphead, * *pphead就可以改变phead了, * 所以也需要这个二级指针 */ 传值、传引用效率比较: //#include <time.h> //#include <iostream> //using namespace std; // // 创建一个“大对象”:A对象中有一个数组(10000个元素): //struct A { int a[10000]; }; // “大对象传参” -- 传值方式: //void TestFunc1(A aa) {} 传值方式需要进行临时拷贝,实参多大就要拷贝多大 // // “大对象传参” -- 传引用(对象)方式: //void TestFunc2(A& aa) {} 传“别名”就好(传“别名”的类型大小的数据) // //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; //} /* * 使用引用做函数参数还可以提高效率: * * 如果参数是一个很大的对象的话, * 使用 传值 就需要将该“大对象”整个拷贝给函数(形参); * 使用 指针 则传地址四个字节就好, * 使用 引用 的话,则传“别名”就好(传“别名”的类型大小的数据) */ //引用的意义之二:做返回值 函数的传值返回:int Count() //int Count() //{ // //创建变量n: // int n = 0; // // n++; // // //传值返回n: // return n; // /* // * Count函数结束,n就会销毁 // * 所以n在传值返回时, // * 实际返回的是n的拷贝,如果n比较小, // * n就会被拷贝到寄存器中, // * 这个拷贝就会作为Count函数的返回值 // */ //} 函数的传引用返回:int& Count() //int& Count() //{ // //创建变量n: // int n = 0; // // n++; // // //传值返回n: // return n; // /* // * 引用返回的话,会返回n的“别名”(引用变量), // * 引用变量的名字由编译器决定,假设叫做tmp, // * 引用变量tmp和n共用同一地址,所以返回“别名”tmp时, // * 所以就相当于返回了n // */ //} // // //int main() //{ // int ret = Count(); // /* // * Count()函数是传引用返回的情况下, // * 最后返回的“别名”的值,即ret接收的值是不确定的, // * ret接收的值取决于Count函数结束时的栈帧会不会被销毁, // * 如果会被销毁,则ret会接收随机值,如果不会被销毁,则ret接收1, // * 在VS编译器下,Count函数结束后栈帧不会被销毁,所以ret会接收1 // */ // // //打印Count函数的返回值: // cout << ret << endl; // // return 0; //} 加法函数:使用传引用返回(错误示范) //int& Add(int a, int b) //{ // int c = a + b; // // //返回两值相加结果: // return c; // //使用传引用返回: // //编译器会自动生成一个对应的int&类型的引用变量返回 //} 错误示范: //int main() //{ // //调用加法函数,并使用引用变量接收: // int& ret = Add(1, 2); // // //打印相加结果: // cout << "第一次调用Add(1,2)时 ret is : " << ret << endl; // /* // * 第一次可能会打印3,也可能打印随机值, // * 取决于编译器是否会清理Count函数的栈帧, // * VS编译器在调用完函数后不会清理函数栈帧, // * 所以此时会打印3 // */ // // Add(3, 4); //换个值再次调用 // //但这次不用引用变量接收了 // // //再次打印之前的相加结果: // cout << "第二次调用Add(1, 2)但未接收返回值时 ret is : " << ret << endl; // /* // * 这次打印会打印:7, // * 明明第二调用函数时没有将结果赋给ret, // * 但ret还是被改变了, // * 本质原因:ret引用的是函数中临时变量c // * 即ret是Add()函数中临时变量c的引用变量, // * 即使函数调用后被销毁了, // * ret依旧和临时变量c共用同一空间, // * 所以之后再调用函数时即使ret不接受返回值, // * ret依旧能到达c的值 // */ // // //所以出了函数作用域后,返回对象就销毁了, // //不能使用 引用 返回该对象。否则结果是不确定的 // //(结果取决于编译器是否会在函数调用后销毁其栈帧) // // return 0; //} //加法函数:使用传引用返回(正确示范) //int& Add(int a, int b) //{ // static int c = a + b; // /* // * 将之后会被返回的对象设置为静态变量, // * 这时c就不在Add()函数的栈帧中了,而是在静态区中, // * 防止函数调用完后被一起销毁掉, // * 所以接收该返回值的引用变量就不是一个销毁空间的别名了, // * 避免出现调用完该函数后接收的返回值结果不确定的情况 // * // * 注:这样写变量c的话,该代码只会被调用一次 // */ // // /* // * static int c; // * c = a + b; // * 如果是这样写该代码的话, // * c = a + b; 这条代码就会被反复调用, // * 这样之后c的“别名”也会被改变 // *(即使返回值不被接收的情况下) // */ // // //返回两值相加结果: // return c; // //使用传引用返回: // //编译器会自动生成一个对应的int&类型的引用变量返回 //} 正确示范: //int main() //{ // //调用加法函数,并使用引用变量接收: // int& ret = Add(1, 2); // // //打印相加结果: // cout << "第一次调用Add(1,2)时 ret is : " << ret << endl; // /* // * 函数中将返回对象设置为静态变量后, // *(局部的静态变量只会被初始化一次,后面再调用就不会再使用它了) // * ret正常接收一个不会被销毁的变量c的引用变量, // * ret是Add()函数中c的“别名”,所以ret会正常打印3 // */ // // Add(3, 4); //换个值再次调用 // //但这次不用引用变量接收了 // // //再次打印之前的相加结果: // cout << "第二次调用Add(1, 2)但未接收返回值时 ret is : " << ret << endl; // /* // * 因为函数中被返回的变量为静态变量, // * 只会被执行一次,所以这次调用函数但不接收的情况下, // * ret不会改变,这才是传引用的正确使用方法 // */ // // return 0; //} //传值和传引用返回值的效率: #include <time.h> #include <iostream> using namespace std; 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; //} //传引用对比传值,在传参和做返回值时传引用的效率都会比较高 //“引用”做返回值:可以修改返回对象 #include <iostream> #include <assert.h> using namespace std; typedef struct SeqList { int a[100]; int size; }SL; void SLModify(SL* ps, int pos, int x) { //... assert(ps); assert(pos < ps->size); ps->a[pos] = x; } //通过传引用返回,返回对应顺序表中pos下标位置的值(的“别名”): int& SLat(SL* ps, int pos) { assert(ps); assert(pos < ps->size); return ps->a[pos]; } //int main() //{ // SL s; // // //初始化…… // // //顺序表每个位置的值都++: // for (size_t i = 0; i < s.size; i++) // { // SLat(&s, i)++; // //“引用”返回的是顺序表中pos位置那个字符的“别名” // //对“别名”进行修改就是对顺序表中pos位置那个字符的修改 // } // // return 0; //} /* * 引用的价值是做参数、做返回值 * 做参数时,能够让形参的修改影响实参,提高效率 * 做返回值时,可以修改返回值(返回对象),提高效率 */ 常引用: //int main() //{ // const int a = 10; //const赋予常属性,只读 // int& b = a; // //a赋予常属性后,无法被引用,因为常量无法修改 // //这时对a进行引用会导致a的“权限放大” // //从 只读 变成了 可读可写 (“权限的放大”) // // int b = a; //这是可以的 // //这里是把a的值拷贝赋予b,b和a不共用同一空间 // //b是一个新变量,b的改变不会改变a // //(和“别名”不同,“别名”的改变会影响a) // // //权限不允许放大,但允许“权限的平移”: // const int& b = a; //也赋予引用变量常属性即可 // //和 a 一样 只读 (“权限的平移”) // // //权限不仅允许“权限的平移”,还允许“权限的缩小”: // int c = 20; //可读可写 // // const int& d = c; // //从 可读可写 变成了 只读(“权限的缩小”) // // //还可以给常量(如:数字)取“别名”: // const int& e = 10; //给常量取“别名” // //10是常量,只读,所以要赋予引用变量e常属性 // // //引用只要不涉及权限的放大即可, // //平移或缩小都是可以的 // // /* // * 所以引用即可以做const修饰变量的“别名”, // * 也可以做非const修饰变量的“别名”, // * 有时引用的意义不是为了修改引用实体, // * 而只是为了让引用实体多一个“别名” // * 可以进行使用 // */ // // int i = 1; //i为整型 // // double j = i; // //将i隐式转化为浮点型,再赋给j // /* // * 注:类型转化时,i不是直接赋给j的 // * 而是会先产生一个临时变量,i会先给到临时变量, // * 临时变量再给到j // */ // // const double& rj = i; // /* // * 注:类型转化时,i不是直接赋给j的 // * 而是会先产生一个临时变量,i会先给到临时变量, // * 临时变量再给到j,临时变量是具有常属性的 // * 所以这里rj引用的是这个具有常属性的临时变量 // * 所以引用时需要给rj也赋予常属性,实现“权限的平移” // */ //} 产生临时变量: //int main() //{ // int i = 1; //i为整型 // // int x = 0; // size_t y = 1; // //注:size_t相当于unsigned int // // if (x > y) // /* // * 不同类型的对象在进行在进行运算符运算时, // * 会进行整型提升,这里有符号的x会被提升为无符号x, // * 但这不是直接就将x的类型固定变成了无符号, // * 而是会产生一个临时变量,接收无符号的x来代替进行运算符运算, // * 所以这里在比较时本质就是临时变量x和y进行比较(x和y都是无符号的) // * // * 而进行引用操作时是没有产生临时变量的,所以不用担心会引用错 // */ // { // // } // // int* ptr = (int*)i; //直接类型转化也是有产生临时变量的 // // int ii = 10000; //整型变量 // char ch = ii; //将较大的整型变量赋给字符变量 // /* // * (类型转换会发生在类型提升、类型截断中) // * // * 导致类型截断: // * 同理,这里整型的ii也不是直接就截断赋给字符类型的ch的, // * 中间也会产生一个临时变量,临时变量先接收ii被截断后的值, // * 然后临时变量再赋给ch // * // * 只要会发生类型转换,就会产生临时变量,而临时变量有具有常属性 // * 传值传参、传值返回都会生成拷贝 // */ //} //int main() //{ // int a = 10; // // //引用:b并没有开空间(语法上) // int& b = a; // // //指针:ptr是有开空间的(语法上) // int* ptr = &a; // // /* // * 在底层,引用和指针是一样的: // * // * 引用底层: // * 汇编指令 -- lea(取地址):将变量a的地址放到eax中 // * 汇编指令 -- mov(移动):将eax放到引用变量n中 // * // * 指针底层: // * 汇编指令 -- lea(取地址):将变量a的地址放到eax中 // * 汇编指令 -- mov(移动):将eax放到指针变量ptr中 // * // * 所以引用和指针在语法层面上不同,但在底层引用和指针是相同的, // * 看起来好像引用没开空间,指针有开空间, // * 实际上引用底层和指针一样是开了单独空间的, // * 所以在底层是没有引用这个概念的,“引用”也是指针, // * 结论:引用底层是用指针实现的 // * // * 在日常使用中,我们还是要以语法层面为主, // * 认为引用是没有单独开空间的,来进行引用操作, // * 日常使用不要和指针混淆 // */ // // return 0; //} //定义一个ADD宏替换函数: #define ADD(x, y) ((x) + (y)) //注:宏不加分号 //返回:((x) + (y)),内外层都要加括号 //使用内联函数替代宏函数: inline int Add(int x, int y) { int c = x + y; return c; } /* * 内联函数克服了宏函数的缺点, * 不用在意返回时括号的设置,还可以调试 * 同时还有宏函数的优点,不用建立栈帧, * 直接在调用函数的位置就会“展开”内联函数,提高效率 * (release发行版本下才会展开) *(debug测试版本下不在意效率,所以内联函数还是会建立栈帧) *(也可以设置VS编译器的属性,设置内联函数即使debug版本也进行展开) *(这里演示的就是调整属性后内联函数) */ /* * 内联函数有这么多好处,但也不能把所有的函数都写成内联函数, * 内联函数只适合频繁调用的小函数(不超过10行代码,10行左右) * 举一个反例: * 假设有一个100行的“大内联函数”,且对其有10000个调用,那么: * 展开合计变成多少行:100 * 10000(行) * 如果不是内联函数不用展开的话:100 + 10000(行) * 将近相差100倍,那么编译后分别生成的可执行程序的大小就相差很大了 * * 所以为了C++中为了防止出现“大内联函数”的展开,C++中也进行了限制: * 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求 */ //int main() //{ // int ret1 = ADD(2, 3) * 5; // // int a = 1; // int b = 2; // int ret2 = ADD(a | b, a & b); // // ( (a|b) + (a&b) ) // // /* // * 宏函数的缺点: // * 1、使用时容易出错,语法细节多(设置括号) // * 2、宏在预处理阶段就替换了,不能调试 // * 3、没有类型安全的检查 // */ // // /* // * 宏函数的优点: // * 1、不用建立栈帧,提高效率 // */ // // /* // * 所以C++中有: // * enum const inline -> 替代宏 // * enum const -> 替代宏常量 // * inline -> 替代宏函数 // */ // // int ret3 = Add(1, 2); // // return 0; //} //#include "Stack.h" //包含栈头文件 // //int main() //{ // f(10); // /* // * 这里调用f()内联函数需要调用该内联函数的地址, // * 因为在编译时只有该内联函数的声明(头文件中), // * 头文件中有该内联函数的声明,那么编译就能暂时通过, // * 但是到了链接步骤要找该内联函数的地址就找不到了, // * 因为内联函数直接在对应位置就展开了,展开就不需要地址了, // * 所以“内联函数不会生成地址”(生成的地址不会进入符号表), // * 因此内联函数的声明和实现分离会报链接错误 // */ // // func(); // // return 0; //} auto推断变量的类型: //int main() //{ // int a = 0; // int b = a; // // //对变量a的类型进行推导: // auto c = a; // //auto可以自动推导出变量a的类型 // // //对地址(指针)的类型进行推导(隐式): // auto d = &a; // //也可以推导出指针类型(隐式) // // //对地址(指针)的类型进行推导(显示): // auto* e = &a; // //和 auto d = &a; 是一样的(显示) // // //对引用变量的类型进行推导: // auto& f = a; //f为a的“别名” // f++; // // // //typeid可以打印一个对象的类型: // cout << typeid(c).name() << endl; //打印c的类型 // cout << typeid(d).name() << endl; //打印d的类型 // cout << typeid(e).name() << endl; //打印e的类型 // // cout << typeid(f).name() << endl; //打印f的类型 // /* // * 引用变量的类型为int,而不是int&, // * 因为在语法层面,f为a的“别名”, // * a的类型为int,f作为其“别名”类型也应该是int // */ // // /* // * 虽然auto可以推导变量类型,但实际没有什么使用价值 // */ //} auto不能做函数参数: //void func(auto e) //{ // //} // auto不能做函数返回值: //auto func() //{ // //} // auto省略类型的定义: //#include<vector> //#include<string> // //int main() //{ // //有一些类型很长: // vector<string> v; // vector<string>::iterator it = v.begin(); //原生类型写法 // //类型很长:vector<string>::iterator // // //使用auto自动推导该类型: // auto it = v.begin(); // //省略了原生类型很长的写法,使用更方便 // // //注:需要初始化才能对相应类型进行推导: // auto x; // // //func(5); // // return 0; //} 范围for循环: //int main() //{ // int array[] = { 1,2,3,4,5 }; // // //C语言:通过for循环配合数组下标进行对数组的遍历 // for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) // { // array[i] *= 2; // } // // cout << "当前数组中元素:" << endl; // // //C语言:通过指针遍历数组 // for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); p++) // { // cout << *p << " "; // } // // cout << endl; //换行 // // cout << "使用范围for循环打印数组元素:" << endl; // // //C++:范围for循环 // for (auto e : array) // /* // * auto e : array // * 依次取数组中的值赋值给变量e(类型可改), // * (注:e只是数组元素的拷贝,改变e不会改变数组中的元素) // * 所以这样写范围for循环只能对数组进行打印, // * 而无法修改数组中元素的值 // * // * 变量e的类型定义为auto可以自动推导array数组中元素的类型, // * 这样数组的类型改变,范围for循环遍历时的类型也不用修改, // * 自动判断结束,自动++往后遍历 // */ // { // cout << e << " "; // } // // cout << endl; //换行 // // cout << "使用范围for循环修改数组元素:" << endl; // // //C++:范围for循环 // for (auto& e : array) // /* // * auto& e : array // * 而如果将变量e的类型修改为 auto& 的话, // * 就可以实现对数组元素的修改了, // * 因为此时将数组元素赋给e后,e就是当前元素的"别名"了, // * 两者共用同一块内存空间, // * 所以就可以通过修改e来修改对应数组元素了 // */ // { // //通过e变量修改数组当前元素: // e++; // // cout << e << " "; // } // // cout << endl; //换行 // // return 0; //} 数组做函数参数的情况下, 函数中无法使用范围for循环: //void TestFor(int array[]) //{ // for (auto& e : array) // { // cout << e << endl; // } // // /* // * 这里TestFor函数看似接收了整个数组, // * 但数组传参实际实际是传数组首元素地址, // * 是一个指针,所以函数中不能使用范围for循环对其进行操作 // */ //} //指针空值nullptr(C++11) void f(int) //只写函数参数的类型没写变量 { cout << "f(int)" << endl; //进行类型匹配:如果实参是int则打印“f(int)” } void f(int*) //只写函数参数的类型没写变量 { cout << "f(int*)" << endl; //进行类型匹配:如果实参是int*则打印“f(int*)” } /* * 实参传给形参,可以只写形参的类型而不写变量, * 这样无法通过形参改变实参,但可以完成参数的匹配 */ int main() { f(0); f(NULL); /* * C语言中,NULL实际是一个宏, * 将NULL定义为了0, * 而不是我们常认为的空指针,认为是个指针 */ int* ptr1 = NULL; f(ptr1); /* * C++中,定义了一个nullptr, * 实际类型为((void*)0),是指针类型 */ int* ptr2 = nullptr; //这样可以更直观表达空指针 f(ptr2); return 0; }