【C++】第一章 入门基础(命名空间、内联函数、函数重载、引用)
文章目录一、命名空间命名空间的意义命名空间的定义命名空间的使用二、C 输入 输出iostream 是什么核心对象std::cin / std::cout / std::endl核心运算符流插入和流提取三、缺省参数缺省参数的定义全缺省和半缺省四、函数重载什么是函数重载函数重载的条件函数重载的类型什么不算函数重载五、引用引用的概念和定义引用的特性引用的使用const 引用指针和引用的关系六、内联函数inline什么是内联函数内联函数和宏函数的区别内联函数的特性七、空指针nullptr什么是nullptrnullptr和NULL的区别nullptr的作用补充C/C程序的完整编译流程一、命名空间命名空间的意义在 C/C 中变量、函数、类等标识符会大量存在若都存放在全局作用域极易引发命名冲突比如不同库中出现同名函数 / 变量编译器无法区分。如图,rand和头文件里面的库函数的名称相同,发生了命名冲突因此,命名空间namespace的核心意义就是通过对标识符进行本地化封装为不同模块、库、代码块创建独立的「命名隔离区」。命名空间的定义基础语法// 定义一个名为 MySpace 的命名空间namespaceMySpace{// 命名空间内可以定义变量、函数、类、结构体、枚举甚至嵌套命名空间intglobal_val10;voidfunc(){std::coutMySpace::func()std::endl;}classMyClass{// 类的成员...};// 嵌套命名空间namespaceNestedSpace{voidnested_func(){}}}补充命名空间是开放的不能用private等访问限定符修饰如果一个项目组里面有两个人可以在一个命名空间里面再嵌套两个命名空间用来表示不同人的模块防止后面出现命名冲突。命名空间的使用前置知识编译器在查找一个变量的声明或者定义时默认只会在局部或者全局中查找不会到命名空间去找。所以我们要使用命名空间中的变量或者函数时需要利用域作用限定符::或者using声明完全限定名直接通过「命名空间标识符」访问完全避免歧义intmain(){MySpace::global_val20;MySpace::func();MySpace::NestedSpace::nested_func();return0;}using声明仅引入指定的标识符到当前作用域不会污染全局// 只把 MySpace 中的 func 引入当前作用域usingMySpace::func;intmain(){func();// 直接调用等价于 MySpace::func()// global_val 仍需要 MySpace::global_val 访问return0;}using指令引用整个命名空间把整个命名空间的所有标识符引入当前作用域容易引发命名冲突// 把 MySpace 整个命名空间引入当前作用域usingnamespaceMySpace;intmain(){global_val30;func();return0;}二、C 输入 输出iostream 是什么全称是Input Output Stream输入输出流是 C 的标准输入输出库(类似于C语言的头文件和库函数)核心作用是提供程序与外部键盘、屏幕、文件等的数据交互能力。注意 是 C 的头文件和 C 语言的 stdio.h 是两套体系但部分编译器如 VS 系列会在中间接包含 stdio.h因此即使不写 #include stdio.h 也能使用printf/scanf但其他编译器如 GCC不保证这一点正式开发中不要依赖这个特性。核心对象std::cin / std::cout / std::endl在C语言中我们的输入和输出时通过printf和scanf类似的我们在C中的输入和输出分别是std::cin和std::cout。这三个是 C 输入输出最常用的对象全部定义在stdstandard标准命名空间中必须通过命名空间访问std::前缀 或using namespace std;。核心运算符流插入和流提取流插入运算符用于 cout 输出把数据写入输出流流向屏幕例std::cout Hello 123 std::endl;流提取运算符用于 cin 输入从输入流中读取数据从键盘读取例int a; std::cin a;这两个运算符是 C 流的核心在 C 中被重载为流运算符和 C 语言中的位运算功能完全不同。补充函数重载可以简单理解为改变一个函数的功能但是不改变这个函数的名称。比如这里在C语言中的位运算符就通过函数重载在C中被赋予了不同的功能。两种方式的优缺点cin和cout的最大优势就是操作简单便捷不需要像printf一样还需要指定输出数据的类型。比如整数用d浮点数用f地址用pcout和cin可以自动识别输入输出的数据的类型就不需要写这些了缺陷就是输入和输出比C语言的更加费时费力了在读入大量数据的时候会花费更多的时间在竞赛时可能直接会导致程序超时。补充ios::sync_with_stdio(false); cin.tie(nullptr);竞赛时可以使用这串代码提高cin和cout的速度三、缺省参数缺省参数的定义缺省参数是声明或者定义时为函数的参数指定的一个缺省值。如果在调用这个函数的时候没有给这个函数传值就使用缺省值作为该函数的参数。全缺省和半缺省全缺省就是所有的形参都给缺省值半缺省就是部分形参给缺省值。半缺省的语法半缺省参数必须从最右侧的形参开始指定默认值中间不能有「未指定默认值的参数」隔开。——即「从右往左依次缺省」。正确写法// 合法c有默认值b、a无voidfunc(inta,intb,intc30){}// 合法b、c有默认值a无voidfunc(inta,intb20,intc30){}// 合法全缺省属于半缺省的特殊情况voidfunc(inta10,intb20,intc30){}错误写法// 错误a有默认值b无中间断开voidfunc(inta10,intb,intc30){}// 错误a有默认值b、c无voidfunc(inta10,intb,intc){}重要语法声明与定义分离规则如果函数声明和定义分开写声明在头文件定义在源文件默认值只能写在声明中不能写在定义中。// 头文件 .h声明中写默认值voidfunc(inta10,intb20);// 源文件 .cpp定义中不能再写默认值voidfunc(inta,intb){// 函数体}缺省参数不能与函数重载冲突缺省参数会生成「隐式重载」如果和显式重载冲突会引发二义性// 错误两个函数在调用func()时无法区分voidfunc(inta10){}voidfunc(){}intmain(){func();// 编译报错二义性不知道调用哪个return0;}四、函数重载什么是函数重载在同一个作用域中函数名相同但是参数不同。函数重载的条件必须同时满足三条在同一个作用域中函数名完全相同参数列表不同函数重载的类型参数类型不同intAdd(inta,intb);doubleAdd(doublea,doubleb);// 构成重载参数个数不同voidf();voidf(inta);// 构成重载参数类型顺序不同voidf(inta,charb);voidf(charb,inta);// 构成重载什么不算函数重载只有返回值不同voidf();intf();// 错误不算重载只有参数名不同voidf(inta);voidf(intb);// 不算参数名不影响不同作用域namespaceA{voidf();}namespaceB{voidf();}// 不算重载是两个独立函数五、引用引用的概念和定义概念引用就是给一个已存在的变量起 “别名”。它不是新变量不开辟新内存和原变量共用同一块空间。定义语法类型引用名已存在的变量名;比如引用的特性在定义时必须初始化不能只写int ra必须绑定一个实体。一个变量可以有多个引用引用一旦绑定就不能再改变指向比如这里其实是把d的值赋给b引用的使用引用主要有两个使用场景分别是引用传参和引用返回引用传参不用解引用代码更简洁、更安全。voidSwap(intx,inty){inttmpx;xy;ytmp;}引用返回减少拷贝返回引用可以直接修改原数据效率更高。下面看一个例子。如果是传值返回的话STTop(st1)3;由于传值返回的是一个临时变量 所以这里赋值的时候会报错。临时对象在函数结束后就会销毁这里相当于给一个空指针赋值 如果是引用返回的话STTop(st1)3;返回的就是它本身的地址不会报错栈顶的元素会被修改成3const 引用作用让引用只能读不能改保护数据。规则const对象必须用const引用constinta10;constintraa;// 正确// int ra a; // 错误权限放大普通变量可以用const引用intb20;constintrbb;// 正确因为权限缩小是被允许的临时变量/常量必须用const引用doubled12.34;intrdd;// 正确constintra*3;// 正确a*3和 d的结果存放在一个临时变量里面 而临时变量也具有只能读不能改的特性补充类型转换也会产生临时对象。doubled12.34intid;constintrid;d赋值给i时类型转换会产生临时对象ri引用的是一个临时对象 而临时对象具有常性所以要加const修饰指针和引用的关系相同点都能“间接操作”原变量都能提高效率避免拷贝不同点引用是给一个变量取别名不开空间指针是存储一个变量的地址要开空间引用不能为空但是指针可以为空引用一旦绑定就不能改变方向但是指针可以多次改变引用可以直接访问对象但是指针需要解引用sizeof结果不同引用是所引类型的大小指针是4/8字节32位的平台是4字节64位的平台是8字节补充相对于指针来说引用更加安全不会出现野指针什么的错误六、内联函数inline什么是内联函数内联函数就是用inline关键字修饰的函数。编译器在编译的时候会把函数体直接展开到调用的地方而不是生成call指令去调用。上面这句话怎么理解举个例子。先看一个普通的加法函数。// 普通函数intAdd(intx,inty){returnxy;}intmain(){inta10,b20;intretAdd(a,b);// 调用Add函数return0;}普通函数的执行步骤遇到int ret Add(a, b);指令的时候编译器会生成一条call的汇编指令然后根据call指令就会跳转到Add函数的内存地址执行函数体代码等Add执行完成后再跳回main函数执行后续的代码这个过程还需要额外建立栈帧如果函数需要被频繁调用的话程序的效率会大打折扣。但如果给把Add变成内联函数。// inline 内联函数inlineintAdd(intx,inty){returnxy;}intmain(){inta10,b20;intretAdd(a,b);// 调用内联函数return0;}inline函数的执行步骤遇到int ret Add(a, b);语句时会把Add的函数体直接复制粘贴到要用的位置就等价于inta10,b20;// Add(a,b) 被直接展开成下面这行没有call指令intretab;-这样调用Add函数时就不需要额外建立栈帧了没有任何跳转和栈帧建立的开销当函数如果被频繁调用时性能比起普通函数就会有很大的提升了。补充inline函数本质上是用代码体积膨胀换运行速度提升推荐把短小的、需要高频调用的函数变成inline函数内联函数和宏函数的区别从上面内联函数的作用可以发现内联函数的功能和我们在C语言学过的宏函数很像那么这两者有什么区别既然有了宏函数为什么C还要多次一举设计内联函数呢两者的本质宏函数本质上是文本替换和编译器无关语法#define宏名(参数)表达式示例#defineADD(x,y)((x)(y))原理编译前预处理器把代码中所有ADD(a,b)直接替换成((a)(b))完全不做语法检查。内联函数本质编译阶段的函数优化由编译器处理是真正的 C 函数。语法和示例inline返回值类型 函数名(参数列表){函数体}inlineintAdd(intx,inty){returnxy;}原理编译时编译器把函数体展开到调用处会检查参数的类型同时保留函数的所有语法。内联函数的优势区间宏可能会出现参数被多次计算的错误而内联函数不会。#defineSQUARE(x)((x)*(x))intmain(){inta5;intretSQUARE(a);// 实际替换后((a) * (a))// 执行过程a先自增到6再自增到7最终计算结果5*630return0;}如果写成内联函数inlineintSquare(intx){returnx*x;}intmain(){inta5;intretSquare(a);//执行逻辑a先传值5然后自增为6//函数里面执行 5*5最后的返回值是25return0;}宏需要考虑运算符的优先级否则结果可能会偏离我们的预期。#defineADD(x,y)xy// 如果不加括号intmain(){intretADD(2,3)*4;// 预期(23)*420// 实际替换后2 3 * 4 2 12 14return0;宏是纯文本替换不会自动加括号必须手动给所有参数和表达式加括号否则极易出错}如果写成内联函数inlineintAdd(intx,inty){returnxy;}intmain(){intretAdd(2,3)*4;// 函数调用先计算Add(2,3)5再*420结果完全正确无需手动加括号return0;}宏没有类型检查结果可能会出错#defineMAX(x,y)((x)(y)?(x):(y))intmain(){inta10;doubleb20.5;intretMAX(a,b);// 宏直接替换不检查类型隐式转换结果可能不符合预期return0;}如果写成内联函数inlineintMax(intx,inty){returnxy?x:y;}intmain(){inta10;doubleb20.5;intretMax(a,b);// 编译器会做类型检查隐式转换double到int可通过重载解决// 若写重载版本inline double Max(double x, double y)则自动匹配类型完全安全return0;}可以发现宏函数在使用的时候极易出现错误而且还需要手动添加括号使用起来很麻烦而内联函数不仅代码简洁而且更加安全这就是C额外设计内联函数的原因了。内联函数的特性inline只是对编译器的建议而不是命令。如果函数太长、递归或者太复杂了编译器也会选择忽略inline当成普通的函数处理。inline函数的声明和定义不能分离否则会出现链接错误。关于为什么不能分离比较复杂简单理解为inline不会生成call指令调用时编译器找不到该函数的函数体。为了方便调试debug模式默认不展开内联函数会当成普通函数去处理只有release为了优化程序会展开内联函数。七、空指针nullptr什么是nullptrnullptr是C引入的关键字专门用来表示空指针的。nullptr和NULL的区别在C语言中NULL实际是一个宏展开C语言的头文件(stddef.h)我们可以看到如下的代码#ifndefNULL#ifdef__cplusplus#defineNULL0#else#defineNULL((void*)0)#endif#endif在C的环境中NULL其实就是整数0在C语言的环境中NULL其实是一个地址0x000000nullptr的作用由于C把NULL定义为0所以在使用时可能会出现如下的错误。NULL本质就是0所以两者最后打印的都是f(int 0)。因此在C需要使用空指针的时候建议全部替换成nullptr。补充C/C程序的完整编译流程程序要从代码到可执行文件需要经历下面四个阶段预处理 → 编译 → 汇编 → 链接下面分别解释每个阶段的作用预处理处理处理#include、#define、#ifdef等预处理指令展开宏、插入头文件内容。完成后输出.i文件编译把代码翻译成汇编代码做语法、语义检查。完成后输出.s文件汇编把汇编代码翻译成机器可以识别的二进制指令生成目标文件.obj每个.cpp源文件都会生成一个独立的目标文件链接把多个目标文件.o/.obj 系统库如libc.so合并成一个完整的可执行文件。