从 C 转 C++?先吃透这些语法升级点:重载 / 引用 / 内联函数详解
🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然
文章目录
- 一、C++与C语言的关系
- 二、命名空间
-
- 1.命名空间的意义
- 2.命名空间的定义
- 3.命名空间的使用
- 二、C++输入&输出
- 三、C++的第一个程序
- 四、缺省参数
- 五、函数重载
-
- 1.C语言函数的弊端
- 2. 函数重载的概念及使用
- 六、引用
-
- 1.引用的概念和定义
- 2.引用的特性
- 3.引用的使用
- 4.const引用
- 5.指针和引用的关系(面试题)
- 七、 inline (内联)
- 八、nullptr(空指针)
- 总结
一、C++与C语言的关系
C++兼容C语⾔绝⼤多数的语法,所以C语⾔实现的hello world依旧可以运⾏,C++中需要把定义⽂件代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤g++编译,不再是gcc,那么为什么要重新创立一门语言来代替C呢?
显而易见是为了解决C语言的一些不足
二、命名空间
1.命名空间的意义
在C / C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题
在这段代码中,由于rand是stdlib里包含的一个函数,我们在全局域里定义rand这个变量会和stdlib冲突,所以命名空间由此诞生!!!
#include #include int rand = 10;int main(){ // 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数” printf(\"%d\\n\", rand); return 0;}
2.命名空间的定义
定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对 { } 即可, { }中即为命名空间的成员。命名空间中可以定义变量/函数/类型等(namespace后面的名字可以任意填写)
namespace本质是定义出⼀个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下
⾯的rand不在冲突了
下述代码访问的是全局的rand函数,而不是命名空间的,因此我们要使用%p接受地址。那么该如何访问命名空间的rand?
“::”被称为域作用限定符
我们可以通过命名空间的名字::命名空间的成员来查找,如hxx::rand
#includenamespace hxx { int rand=5;}int main(){ //这时候问的是全局的rand函数,而不是命名空间的,因此我们要使用%p接受地址 printf(\"%pn\",rand); //打印的是命名空间里的rand printf(\"%d \",hxx::rand); return 0;
上面提到了不同的域可以定义同名变量,那么在main函数中先调用的是局部变量,打印结果为5
我们想要调用全局的变量,就可以使用域作用限定符来修饰变量,如 “::a”,限定符左边什么都不写默认是从全局域查找,当然也可以不写,因此默认如果局部没有该变量会在全局找,而不是去命名空间找,因此命名空间需要使用域作用限定符
#includeint a=10;int main(){ int a=5; printf(\"%d \",a); return 0;}
命名空间注意事项
•C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期
• namespace只能定义在全局,当然他还可以嵌套定义。如果在公司中,把一个部门的人分为多个项目组,每个项目组规定使用不同的命名空间防止冲突,假设项目组里面的几个人定义的命名冲突了这里的命名空间可以嵌套使用,避免冲突
• 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
• C++标准库都放在⼀个叫std(standard)的命名空间中
• 域作用限定符在修饰结构体是通过在结构体名前添加 “作用域前缀::”,如 struct hxx::Node p1;
3.命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找
所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很大,日常小练习程序为了方便推荐使⽤。
1.指定命名空间访问
namespace hxx{ namespace baoni { int a=10; int Add(int left, int right) {return left + right; }}}int main(){ printf(\"%d\\n\",hxx::baoni::Add(1,1));}
2. 2和3场景类似,这里一起举例子
namespace hxx{int a = 0;int b = 1;}using namespace hxx;//展开命名空间中全部成员,项目不推荐,冲突风险很大,日常练习为了方便可以使用//这里也可以不全部展开命名空间的成员using hxx::a;//这里只将命名空间中的a展开使用
二、C++输入&输出
以上是C++的输入输出的概念,后面讲述到面向对象的时候将会深入了解,这里只做简单介绍。
#includeusing namespace std;int main(){ int a=0; double b=0.1; char c=\'x\'//C++的输入输出可以自动识别变量类型 cout<<a<<\" \"<<b<<\" \"<<c<<endl; cout<<&a<<endl; cin>>a; cin>>b>>c;}
由于C++的IO流需要兼容 C语言,因此会付出一些代价(和缓冲区相关的知识),因此在竞赛中我们需要高IO需求的,使用cout,cin,OJ题过不了(性能被卡住),所以加上以下三行代码来提高C++ IO效率,当然也可以直接使用printf或者scanf函数
#includeusing namespace std;int main(){ // 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码 // 可以提⾼C++IO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); return 0;}
三、C++的第一个程序
我们经过上面两个概念便可以写出引领我们第一次进入编程语言的C++版本Hello World!!!
#includeusing namespace std;int main(){cout<<\"Hello World!\"<< endl;return 0;}
四、缺省参数
• 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
• 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
• 带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
• 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值(为了防止申明和定义给的缺省值不同,不明确以谁为准)
#includeusing namespace std;Func(int a=0){cout<<a<<endl;}//全缺省void Func1(int a=10,int b=20,int c=30){cout<<\"a= \"<<a<<endl;cout<<\"b= \"<<b<<endl;cout<<\"c= \"<<c<<endl; }//半缺省,半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值void Func2(int a,int b=10,int c=20){ cout<<\"a= \"<<a<<endl; cout<<\"b= \"<<b<<endl; cout<<\"c= \"<<c<<endl;}int main(){ Func(10);//没有传参时,使用参数的默认值 Func();//传参时,使用指定的实参 //全缺省有四种调用方式 Func1(); Func1(1); Func1(1,2); Func1(1,2,3); //半缺省必须传参,一个都不传会报错 Func2(100);return 0;}
缺省参数带来的优点
在C语言我们实现栈的初始化和入栈时我们需要判断并且扩容,会降低实现效率,但是我们有了缺省参数时,在不知道需要多少空间,我们给缺省参数即可,明确了空间大小,便可以直接给定空间大小,避免不断扩容带来的问题
我们可以把缺省参数看成是在生活中的备胎,当需要你的时候出现,不需要就不出现,没有意义,因此我们要做就要做实参!!!
五、函数重载
1.C语言函数的弊端
C语言中一个函数只能传固定格式类型的参数
#includevoid Add(int a,int b){ return a+b;}int main(){ Add(1,2);//这里不可以传浮点数等其他类型的参数}
我们发现我们想要传浮点数或者其他类型,便需要再重新定义一个不同名函数,这在函数调用时便很繁琐,因此我们便在C++中有了函数重载这个概念
2. 函数重载的概念及使用
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
下面我们通过函数重载来实现下类型不同
#incude<iostream>int Add(int a,int b){ return a+b;}double Add(double a,double b){return a+b;}double Add(int a,double b){return a+b;}double Add(double a,int b){return a+b;}int main(){ Add(1,1.2); Add(1,2); Add(1.2,1.2);}
参数个数不同
void f(){ cout << \"f()\" << endl;}void f(int a){ cout << \"f(int a)\" << endl;}
参数顺序不同
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;}
返回值不同不构成函数重载,因为调用时无法区分
void fxx(){}int fxx(){ return 0;}
我们需要注意下下面这段代码,它们符合函数重载的形式,但是在调用时因为缺省参数可以省略,会产生歧义
void f1(){ cout << \"f()\" << endl;}void f1(int a = 10){ cout << \"f(int a)\" << endl;}int main(){ f1(); return 0;}
我们发现代码报错,函数调用不明确,这便是在函数重载中使用缺省参数的问题(因为缺省参数不给参数也可以使用)
如果我们想强制这么使用,那么我们可以把一个函数放到命名空间即可,但是这里便不构成函数重载了,因为需要再同一作用域下才可以
总结:
我们发现我们使用函数重载会很爽,感觉是在使用一个函数,这便是函数重载的优点!!!
六、引用
1.引用的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫\"铁⽜\",江湖上⼈称\"⿊旋⻛\";林冲,外号豹⼦头;
类型& 引⽤别名 = 引⽤对象;
引用的出现是为了优化指针在某些场景下的的繁琐,而不是为了取代指针,和指针是相辅相成的
#includeusing namespace std;int main(){ int a = 0; // 引⽤:b和c是a的别名 int& b = a; int& c = a; // 也可以给别名b取别名,d相当于还是a的别名 int& d = b; ++d; // 这⾥取地址,我们看到是四个均是⼀样的 cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0;}
再比如我们有个空间取名为a,给它取了别名b,c,当然也可以给别名取别名如d,它们的地址均是相同的
2.引用的特性
• 引⽤在定义时必须初始化(必须说是谁的别名)
• ⼀个变量可以有多个引⽤
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体(a=b是赋值而不是再引用)
#includeusing namespace std;int main(){ int a = 10; // 编译报错:“prev”: 必须初始化引⽤ //int& prev; int& b = a; int c = 20; // 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向, // 这⾥是⼀个赋值 b = c; cout << &a << endl; cout << &b << endl; cout << &c << endl; return 0;}
3.引用的使用
• 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
• 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些
• 引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解
• 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向
我们通过代码来感受下引用的魅力!!
#includeusing namespace std;typedef struct SeqList{ int a[10]; int size;}SLT;// ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针void SeqPushBack(SLT& sl, int x){}typedef struct ListNode{ int val; struct ListNode* next;}LTNode, *PNode;// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名 // 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序 //void ListPushBack(LTNode** phead, int x)//void ListPushBack(LTNode*& phead, int x)void ListPushBack(PNode& phead, int x){ PNode newnode = (PNode)malloc(sizeof(LTNode)); newnode->val = x; newnode->next = NULL; if (phead == NULL) { phead = newnode; } else { //... }}int main(){ PNode plist = NULL; ListPushBack(plist, 1); return 0;}
4.const引用
• 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。(权限缩小的对象并不会让被引用的权限缩小),const引用也可以引用常量如:const int & rb=30;(普通引用不行),普通引用不可以引用表达式,因为表达式有返回值,返回值存在临时变量里面,临时变量具有常性,因此可以使用const引用,如:const int & c=(a+b);
• 不需要注意的是类似 int& rb = a 3; double d = 12.34; int & rd = d; 这样⼀些场景下 a3 的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
• 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象
总结:类型转换,表达式运算结果,调用函数,函数传值返回会产生临时变量
5.指针和引用的关系(面试题)
在最后一点中,引用并不是绝对安全的,如下引用局部变量返回:当函数func()执行结束时,局部变量a会被销毁(其内存空间被释放),此时返回的引用就变成了 \"悬空引用”
int& func{ int a=0; return a;}
再如下面的空引用
int main(){int* ptr = NULL;int& rb = *ptr;rb++;//退出码不为0即代码有问题}
七、 inline (内联)
• ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略,,因为展开后会导致可执行程序变大
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数,比如使用宏来实现两数相加函数,面试也喜欢考和宏相关的知识
// 实现⼀个ADD宏函数的常⻅问题 //#define ADD(int a, int b) return a + b;//#define ADD(a, b) a + b;//#define ADD(a, b) (a + b)// 正确的宏实现 #define ADD(a, b) ((a) + (b))// 为什么不能加分号? // 为什么要加外⾯的括号? // 为什么要加⾥⾯的括号? int main(){int ret=ADD(1,2);//相当于将右边替换成 ((a) + (b)),也就是((1)+(2)); //第一点因为宏函数是函数,因此在加分号后,使用时比如在cout中也会含有,会报错,再比如进行if条件判断也是会报错 cout<<ADD(1,2)<<endl; //第二点要加外面括号可以控制运算时的优先级,在下面示例中,若如不加,我们期望是1+2=3,3*5=15,但是*的优先级会更高,输出结果为2*5+1=11 cout<<ADD(1,2)*5<<endl; //第三点要加里面的括号也是控制优先级 int x=1,y=2; ADD(x & y,x | y);//->(x&y+x|y),因为与和或的优先级低于+-,因此如果不加里面的括号结果便变为先y+x,再进行与和或的运算 return 0;}
因此我们发现宏很复杂,稍不留神或者忘记变会出错,因此C++提出了内联,下面我们把上述的Add函数写成内联来感受下其魅力
inline Add(int x,int y){ C return ret;}int main(){ int ret=Add(1,2); cout<<Add(1,2)*5<<endl; return 0;}
• vs编译器debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错
八、nullptr(空指针)
在介绍nullptr前,我们需要先回顾下C语言中的NULL,NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif#endif
• C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些麻烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。
using namespace std;void f(int x){cout<<\"f(int x)\"<<endl;}void f(int* ptr){ cout << \"f(int* ptr)\" << endl;}//上面构成了函数重载int main(){ f(0); // 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。 f(NULL); f((int*)NULL); // 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型 // f((void*)NULL); f(nullptr); return 0;}
在C语言中void可以隐式转换成其他类型的指针,在C++中void不可以给给其他类型的指针,因此必须强转,否则报错
int main(){ void*p1=NULL; int*p2=p1;}
因此在C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型
总结
综上,本文梳理的命名空间、函数重载、引用、内联函数等特性,是 C++ 对 C 语言的关键升级 —— 既兼容底层逻辑,又通过语法优化解决了命名冲突、代码冗余等痛点。这些基础是后续类与对象、STL 等进阶内容的基石,吃透它们,就能更顺畅地迈入 C++ 面向对象编程的大门