目录
一.命名空间和输入输出流
1.命名空间
a.命名空间的由来
在C语言中,当代码量过多时,就可能产生命名冲突问题,如:与库中的函数名冲突、与他人文件中全局变量、函数名称冲突。这种名称冲突防不胜防,而且修改起来颇为麻烦,所以,为了解决这一麻烦,我们在C++中引入命名空间。
那么,什么是命名空间?
命名空间是一种封装 标识符(如变量名、函数名、类名等)的空间域,我们可以在命名空间域内定义变量、函数或类等,使用时通过使用域作用访问限定符 来避免命名冲突问题, 它还可以帮助组织代码,使其更加模块化和易于维护。
b.命名空间的使用
①命名空间的定义
namespace myspace{} ------ 定义一个命名空间域,域名为myspace.
②命名空间的展开
using namespace bit; ------ 将名为bit的命名空间域展开
③域作用访问限定符
访问格式 ------ 域名 + " : : " + 目标名
name :: a 域作用限定符,若左边有域名,则去左边的命名空间域 内访问该变量;若左边不写域名,则默认访问的是全局域。
在一个函数内部,对某一变量的访问优先级 是:访问指定的命名空间域------> 局部域------>全局域和展开了的命名空间域
展开命名空间并不是将命名空间里的数据拷贝到当前,而是决定了编译器编译时是否去展开的命名搜索,展开了的命名空间域内的变量拥有全局属性
命名空间的特性:
a.命名空间内可以定义变量、函数、类型;
b.命名空间可以嵌套;
c.在同一个文件中,可以存在多个域名相同的命名空间,它们在编译时会被整合到一起。
2.输入输出流
a.基本概念
流:在C++中,数据像流水一样从一个地方流动到另一个地方,因此将输入输出称为"流"。输入输出流是指由若干字节组成的字节序列,这些字节中的数据按顺序从一个对象传送到另一对象。
输入流 :将输入的字符序列形式的数据变换成计算机内部形式的数据(二进制或ASCII)后,再赋给变量。输入操作时,字节流从输入设备(如键盘、磁盘)流向内存。
输出流 :将要输出的数据变换成字符串形式后,送到输出流(文件)中。输出操作时,字节流从内存流向输出设备(如屏幕、打印机、磁盘等)。
b.标准输入输出流
cin :标准输入流对象,用于从标准输入设备(通常是键盘)读取数据。使用**>>**
操作符从输入中提取数据到变量。
cout :标准输出流对象,用于输出数据到标准输出设备(通常是显示器)。使用 <<
操作符连接输出项,endl
用于输出换行。
cerr :标准错误输出流对象,通常用于输出错误信息。与显示器关联,但缓冲区满或遇到endl
时才向显示器输出,且不能被重定向。
<< ------ 流插入运算符; >> ------ 流提取运算符;
举例:
std :: cin >> str ; 本质是从键盘读取我们输入的数据,然后将其写到 str 变量中保存。
std :: cout << "hello world" << std :: endl ; 本质是将字符串打印到屏幕上。
二.缺省参数和函数重载
1.缺省参数
缺省参数是指在声明或定义函数 时,为函数的某些或全部参数 指定一个默认值。在调用该函数时,如果没有为这些参数提供实参,则采用这些参数的缺省值;如果提供了实参,则使用指定的实参。
a.全缺省
函数的所有参数都设定一个缺省值,如:
void func(int a=10, int b=20, int c=30) { }
func(1,2);
注意:调用函数时,所传参数按从左到右给形参赋值!
b.半缺省
半缺省参数必须从右往左 依次给出,不能间隔着给,如:
void func(int a, int b=10, int c=20);
func(1,2);
调用函数时,所传参数按从左到右赋值给形参~~
缺省值必须是常量或者全局变量。
注意:函数的声明和定义不能同时给缺省值,且在VS中,只能是函数的声明给缺省值,函数的定义不能给缺省值,否则编译器会报错!!
2.函数重载
C++允许在同一作用域 中声明几个功能类似的同名函数 ,这些同名函数的形参列表(参数个数、类型或类型顺序,注:返回值不算)不同,常用来处理实现功能类似而数据类型不同的问题。
注:void func(); 和 void func(int a=10); 二者构成函数重载,但调用func()时,编译器依旧会报错,因为该函数的调用不明确。
函数名修饰规则
*C语言为何无法实现函数重载,C++又是如何支持函数重载的?? --- 编译链接过程 + 函数名修饰规则!
在Linux系统下:
在gcc编译器(编译C语言代码)编译的过程中,编译器为函数生成地址时,只认函数名作唯一的判断标准;
在g++编译器(编译C++代码)编译的过程中,编译器在为函数生成地址(汇编过程,符号表汇总) 时,有独特的函数名修饰规则 ,即用前缀+函数名长度+函数名+参数类型+...等等 为判断标准。
如:对void func(int a)和void func(int a, double b) 编译生成的函数标识分别是 <_Z 4funci> 和 <_Z4funcid>,其中:_Z是前缀,4是函数名长度,func是函数名,i和id是参数类型。
为什么无法以返回值类型用作函数重载的判断标准?--- 函数调用歧义问题~~
如:int func(); double func(); int main() { func(); }
这种情况下,编译器就无法判断到底调用哪个 func() 函数,出现函数调用歧义问题!
小知识:在Linux命令行中:objdump -S test.exe 指令 可以查看test.exe形成时的汇编过程
三.引用和范围for
1.引用
引用,其实就是给一个已存在的变量取了个别名,通过引用,我们可以直接操作原始数据,而无需进行数据的拷贝 。引用在函数参数传递和返回值时,可以大大减少数据拷贝所带来的开销~~
a.引用的定义和初始化
& 我们可以在变量类型 后添加**&** 符号来进行引用对象的定义与声明。例如,int& ref = var;表示 **ref
**是 var 的引用。
注意: 引用在创建时必须被初始化,且一旦初始化后,就不能再改变引用的目标 。这是因为引用在底层实现上通常是一个指针常量,指向其目标变量。
通过引用,我们可以直接修改目标变量的值。这种特性使得引用在函数参数传递时非常有用,因为它允许函数直接修改调用者提供的变量。
引用的大小 :在大多数实现中,引用的大小与其所引用的对象类型无关,且通常与指针的大小相同 。这是因为引用在底层通常是通过指针来实现的。
b.引用的使用场景
①引用**传参(**输出型参数,面对大对象/深拷贝对象,可以提高效率)
②引用做返回值 (面对大对象/深拷贝对象,可以提高效率)
传值返回,会生成临时变量,发生两次拷贝;而传引用返回则不会生成临时变量!(*条件 :使用引用返回时,返回值的生命周期不能属于该函数!!)
若引用返回的返回值是函数的局部变量:
若果函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的;
如果函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值。
**注意:没有空引用,**引用必须指向一个有效的对象,不能为空。这与指针不同,指针可以指向空。
常引用
例如:const int& m = 10; (10是常量,所以要m加const修饰,否则会导致权限放大问题)
引用的过程当中,权限不能放大,只能平移或缩小。对权限缩小的引用来说,权限缩小的是引用的别名,不会影响到原变量本身。
举例:
这是为啥??--- 类型转换会出现临时变量 ,即将dd的值拷贝给临时变量,然后将临时变量转化后的值再拷贝给ii,但是临时变量具有常属性,所以拷贝给别名时要加const修饰!!
c.引用和指针的区别
①引用必须在*定义时初始化,指针没有要求;
②引用在初始化一个实体后,就不能再引用其它实体,而指针可以在任何时候指向任何一个同类型实体;
③没有NULL引用,但有NULL指针;
④引用对指向的对象无需进行取地址和解引用操作,但指针需要。
2.auto和范围for
a.auto关键词
auto 关键字用于自动类型推导。当编译器能够确定变量的类型时,可以使用 auto 来让编译器自动推断变量的类型,而无需显式指定。
使用场景:
①迭代器和循环 :
在使用容器(如 vector、list、map等)的迭代器时,迭代器的类型通常很长且复杂,所以,我们可以使用 auto 来简化代码:
②lambda 表达式 :
在定义 lambda 表达式时,使用 auto 可以避免显式指定 lambda 的类型:
③类型推导 :
在模板编程中,auto 可以用于推导模板参数的类型:
cout<<typeid(ref).name()<<endl; 可以打印出ref的类型 .
注意:
①auto 只能用于局部变量和函数参数(C++11 及以后),不能用于全局变量或类成员变量。
②使用 auto 时,变量的初始化是必须的,因为编译器需要通过初始化表达式来推导类型。
③当使用 auto 与引用或指针结合时,需要特别注意推导的结果。
例如,auto& x =
someExpression;
会推导 x 为 someExpression的引用类型。
b.范围for
四.内联函数
1.什么是内联函数?
内联函数是一种特殊的函数,它建议 编译器在调用该函数的地方直接插入 该函数的代码,而不是像通常的函数调用那样进行跳转。这样做的目的是减少函数调用 的开销(不需要创建栈帧 ),特别是当函数体很小且频繁调用时。
听上去是不是感觉内联函数 与C语言里的宏函数类似?它们有什么区别?
a.内联函数与宏函数的共同点
①内联函数和宏函数都是为了减少函数调用的开销,从而在一定程度上提高程序的执行效率。
②两者都通过以空间换时间 的方式来实现性能优化。内联函数会在每个调用点复制函数体代码 ,而宏函数则通过文本替换来避免函数调用的开销。
b.内联函数与宏函数的区别
①宏函数是在预处理阶段 进行展开,而内联函数是在编译阶段进行展开,编译器会在每个调用内联函数的地方将内联函数的代码直接插入到调用点。
②宏函数没有类型检查 ,因为宏只是简单的文本替换,这可能导致在宏替换后产生类型不匹配的错误,导致运行报错。而内联函数有类型检查,因为内联函数是真正的函数,编译器在编译时会对其进行类型检查,一旦出现类型不匹配问题,编译阶段就会有语法报错。
③ 由于宏函数 只是文本替换,所以无法调试 ,且为保证执行逻辑严谨,通常要写很多括号,容易出错且可读性差;而内联函数具有函数的特性,可以像普通函数一样进行调试,还容易读懂。
④宏函数 直接通过替换传参,传递的是实参本身。由于没有函数调用栈的开销,宏函数在执行速度上通常更快。但这也可能导致一些意外的行为,如运算符优先级问题。内联函数通过常规压栈方式传参,可以传值也可以传引用。内联函数在传参时的开销相对较大,但由于有类型安全检查,代码更加安全。
所以,我们可以说,内联函数就是C语言中,宏函数的升级版~~
2.内联函数的使用
内联函数可以通过在函数定义前加上 inline 关键字来声明。例如:
注意:并不是我们在函数前加一个 inline 就能使其成为内联函数。inline 只是我们给编译器的一个建议,被修饰的函数是否真的是内联函数,最终还是由编译器说了算~~
注意:内联函数仅适用于代码短小(递归、循环、分支除外)且被频繁调用的函数!!
代码过长的函数或递归函数,搞成内联形成的话,容易造成代码膨胀问题,最终造成可执行程序变大!!
注意:对于内联函数,建议将定义和声明放在同一个文件中 ,最好是在头文件 中,内联函数在汇编时不会生成地址 ,也不会进入符号表 ,所以内联函数的***** 声明和定义不能分开, 否则链接的时候找不到~~
debug版本下的inline不会起作用(编译器通常会忽略内联建议),否则不方便调试~~
小知识:
C++98 :#define NULL 0
C++11:#define nullptr ((void*)0)