目录
1.3 域的访问【局部域>全局域>展开了的命名空间域("using namespace N;",该域暴露在全局中)or指定访问命名空间】
1. 命名空间
1.1 为什么要用命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
//1.普通的命名空间
namespace N1 //N1为命名空间的名称
{
//命名空间中的内容,既可以定义变量,也可以定义函数
int a;
int Add(int left,int right)
{
return left+right;
}
}
//2.命名空间可嵌套
//命名空间嵌套的需求:命名空间内也可能冲突
//命名冲突包括1.库冲突和2.项目之间冲突
namespace N2
{
int a;
int b;
int Add(int left,int right)
{
return left+right;
}
namespace N3
{
int c;
int d;
int Sub(int left,int right)
{
return left-right;
}
}
}
//3.同一个工程中,允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
namespace N1
{
int Mul(int left,int right)
{
return left*right;
}
}
//需要注意的是一个命名空间相当于定义了一个新的作用域,命名空间内的所有内容都局限在该命名空间中。
1.2 命名空间的使用
a.加命名空间名称以及域作用限定符使用
int main()
{
printf("%d\n",N::a);
return 0;
}
b.使用using将命名空间中的某个成员引入
using N::b;
int main()
{
printf("%d\n",N::a);
printf("%d\n",b);
return 0;
}
c.使用using namespace命名空间名称引入
using namespace N;
int main()
{
printg("%d\n",N::a);
printg("%d\n",b);
Add(10,20);
return 0;
}
1.3 域的访问【局部域>全局域>展开了的命名空间域("using namespace N;",该域暴露在全局中)or指定访问命名空间】
int a=0; //全局变量
int main()
{
int a=1;
printf("%d\n",a); //打印的为局部的a
printf("%d\n",::a); //打印的为全局的a,其中::为域作用限定符
return 0;
}
std:c++标准库,其中包含stl与c++库
命名空间的展开表示会不会去这个命名空间搜索,例如不加#include<iostream>等#include<......>,则不不会在#include<iostream>等#include<......>中搜索。
情况一:std::cout<<"hello world"<<std::endl;
情况二:using namespace std;
cout<<"hello world"<<endl;
其中,<<为流插入运算符,>>为流提取运算符,cout与cin都有自动识别类型的特点,c与c++可混编。
tip:具体做项目时不建议直接像情况二一样全部展开,不然会有风险,如果与库命名冲突则会报错,建议具体项目中不展开(日常练习可以展开),做项目时部分展开,例如using std::cout。
2.缺省参数
2.1 概念
缺省参数是声明或定义函数时为函数的参数确定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a=0)
{
cout<<a<<endl;
}
int main()
{
Func(); //没传参时,使用参数的默认值,即a=0
Func(1); //传参时,使用指定的实参,即a=1
return 0;
}
2.2 两种类型的缺省参数
2.2.1 全缺省参数
void Func(int a=10,int b=20,int c=30)
{
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
2.2.2 半缺省参数
void Func(int a,int b=20,int c=30)
{
cout<<a<<endl;
cout<<b<<endl;
cout<<c<<endl;
}
Tip:
1.半缺省参数必须从右往左以此给出,不可以间隔给值。
2.缺省值必须为常量或者全局变量。
3.C语言的编译器不支持缺省参数。
4.缺省参数不能在函数声明和定义中同时出现。
//test.h
void Func(int a=10); //函数的声明
//test.cpp
void Func(int a=20) //函数的定义
{}
//如果声明和定义位置同时出现缺省值,且两处值不同,编译器会无法识别到底该用哪个缺省值
3.函数重载
理解:自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,那么就可以认为该词被重载了。
3.1 函数重载的概念
函数重载为函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(1.参数个数 或 2.类型 或 3.类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
3.2 C++支持函数重载的原理(名称修饰)
-
名称修饰(Name Mangling):函数重载需要在编译过程中对函数名进行名称修饰,以添加关于参数类型、顺序和个数等信息,从而区分不同的函数。然而,在 C 语言中,没有提供名称修饰机制,函数名在编译后直接映射到可执行代码中,不包含额外的信息。
-
兼容性:C 语言是一种面向底层编程的语言,与汇编语言和机器码有较强的兼容性。因此,C 语言的设计目标是保持简单、直接和高效,避免引入复杂的语法和额外的处理逻辑。函数重载会增加编译器的复杂度,不符合 C 语言的初衷。
-
编译器设计:C 语言的早期编译器设计主要集中在快速生成高效的汇编代码上,没有将函数重载的需求作为首要考虑。在 C 语言标准制定的时候,函数重载并不是一个普遍存在或必要的特性。
在c/c++中,程序运行起来要经历预处理,编译,汇编,链接四个阶段。
例如:list.h,list.c,test.c(项目一般由多个头文件.h和多个源文件.cpp构成,)
预处理阶段:将头文件展开/宏替换/条件编译/去掉注释。(成为list.i,test.i)
编译阶段:检查语法,生成汇编代码。(成为list.s,test.s)
汇编阶段:将汇编代码转为二进制机器码。(成为list.o,test.o)
链接阶段:将两个目标文件链接到一起。(举个例子:在汇编阶段生成的.o文件,如果test.o中调用一个函数但是在test.o中找不到该函数地址,那么链接器就会到list.o文件中去找)
对于链接时,test.o会使用哪个名字去查找,举一个linux下g++的例子:
用gcc测试(gcc -o testc test.c):函数名修饰不会变。
用g++测试(gcc -o testcpp test.cpp):函数名的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
总结来说,在Linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中,变成"_Z+函数长度+函数名+类型字母"。
综上,c语言不支持函数重载,但是c++支持函数重载。(函数名修饰规则不一样,可能会导致链接失败)
4.extern "C"
因为c和c++编译器对函数名字的修饰规则不一样,故在某些场景下会出现问题
1.c++中调用c的静态库或者动态库,c调用c++的静态库或者动态库。
2.c与c++混编时。
故引入extern"C"来告诉编译器,按照C语言规则来编译。
#ifdef __cplusplus
extern "C" {
#endif
// 在这里放置C风格的函数声明或定义
#ifdef __cplusplus
}
#endif
5.引用
引用不是新定义一个变量,而是给已经定义的变量一个别名,编译器不会额外为其开辟空间,它和它的变量共用同一片空间。
5.1 引用特点
1.引用在定义的时候必须初始化。
2.一个变量可以有多个引用。
3.引用名只可对应一个引用变量,不可对应多个。
5.2 常引用
void TestConstRef()
{
const int a=10;
//int& ra=a; //语句报错,变为ra后权限放大,a具有常属性,不可修改
const int& ra=a; //正确
//int& b=10; //10为字面量常量,具有常属性,而引用类型b为可修改,故错误
const int& b=10; //正确
double d=11.11;
//int& rd=d; //类型不符,错误
const double& rd=d; //正确
}
5.3 引用类型的使用
5.3.1 引用做参数
-
直接操作原始对象:通过将参数定义为引用,函数可以直接操作原始对象,而无需对对象进行拷贝或创建副本。这样可以提高效率并避免不必要的内存开销。
-
改变原始对象的值:通过引用参数,函数可以修改原始对象的值,这样可以实现在函数内部对原始对象进行更改,并使修改在函数外仍然有效。
-
传递大型对象的效率:将对象作为引用参数传递,比将对象作为值参数传递更高效。因为不涉及拷贝操作,尤其对于大型对象来说,可以显著提高性能。
-
提供多个返回值:函数可以同时返回多个值,将其作为引用参数传递给函数,并在函数内对引用参数进行修改。
5.3.2 引用做返回值
- 避免拷贝:使用引用作为返回值,可以避免大型对象的拷贝操作,提高性能和效率。
- 内存安全:通过返回引用,可以确保返回值与原始对象共享相同的内存空间,避免了不必要的内存分配和释放。
- 支持连续赋值:由于返回的是引用,可以将多个赋值操作连续执行,提高代码的简洁性和可读性。
- 可修改原始对象:通过返回引用,可以直接修改原始对象的值,而无需再次传递和修改副本。
需要注意的是,如果函数返回时,出了函数作用域,若返回的对象生命周期还在,则可以使用引用返回,如果生命周期结束了,则会造成悬空引用,此时只可用穿值返回或者将该对象用static静态修饰。
5.4 传值与穿引用的效率
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
5.5 引用与指针的比较
在底层实现上引用实际是有空间的,因为引用是按照指针方式来实现的。
5.5.1 引用和指针的不同点
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求。
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
4. 没有NULL引用,但有NULL指针。
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)。
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
7. 有多级指针,但是没有多级引用。
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
9. 引用比指针使用起来相对更安全。
6.内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧 的开销,内联函数提升程序运行的效率。
6.1 特性
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替 换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规 模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、频繁调用的函数 采用inline修饰,否则编译器会忽略inline特性。
3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会 找不到。
question1:宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
question2:C++有哪些技术替代宏?
1. 常量定义 换用const enum.
2. 短小函数定义 换用内联函数.
7.c++与c语言
// 这里仅举一个例子
//要求将数据中的值乘2倍,再打印
//1.c语言中中的做法
for(int i=0;i<sizeof(array)/sizeof(int);++i)
{
array[j]*=2;}
for(int i=0;i<sizeof(array)/sizeof(int);++i)
{
cout<<array[i]<<" ";}
cout<<endl;
//c++实现->范围for(语法糖)->特点:写起来比较简洁
for(auto&e:array)
{
e*=2;}
for(auto e:array)
{
cout<<e<<" ";}
cout<<endl;