ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

《从C到C++(一):8个让代码更好写的新语法》

《从C到C++(一):8个让代码更好写的新语法》 学完 C 之后很多人第一次打开 C 会觉得「这不就是 C 加了一堆新语法吗」。这话对了一半——C 确实兼容 C但真正让人愿意从 C 切过来的不是那套花哨的语法而是它把 C 里写得别扭、容易出错的地方一个个都改得更顺手了。比如同一个作用域不允许出现同名函数类似逻辑的函数可能需要写很多份要想通过形参改变实参就必须传指针*p用起来既抽象手滑还容易违规报错NULL到底是0还是指针编译器自己也说不清重载的时候经常出幺蛾子想给函数传个默认值C 里只能自己多写一套重载逻辑这些问题C 都给了新的答案。这篇博客不追求讲深只讲一件事C 相比 C多了哪些让你写起来更爽、更不容易踩坑的新语法。我会把 知识点拆成 卡片逐一讲解——命名空间、缺省参数、函数重载、引用、内联函数、nullptr。1命名空间namespace什么是命名空间命名空间本质定义了一个作用域完成对标识符名称的本地化。说人话就是将不同同名标识符分割开从而避免了命名冲突和名称污染。namespace A { void init() {} } namespace B { void init() {} } A::init(); B::init(); // 各调各的细节为什么要设计命名空间命名空间可以很好解决C语言项目中命名冲突和名称污染的问题#include stdio.h #include stdlib.h int rand 10; int main() { // 编译报错error C2365: “rand”: 重定义以前的定义是“函数” printf(%d\n, rand); return 0; }在C语言项目中定义的全局变量很容易和某个函数命名冲突而将这个全局变量定义在一个命名空间中就可以很好的解决这个问题C中有四种域——函数局部域全局域命名空间域类域域影响的是编译时语法查找⼀个变量/函数/ 类型出处(声明或定义)的逻辑。因为语法在编译阶段查找声明或者定义是如果不单独说明只会在局部域和全局域中去找所有有了域隔离名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑还会影响变量的⽣命周期命名空间域和类域不影响变量⽣命周期。namespace可以嵌套定义。namespace school { namespace class1 { int a 1; } namespace class2 { int a 2; } }这样就解决了一个命名空间中命名冲突的问题项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace不会冲突。多个文件中定义的同名namespace它们会被默认合并在一起就像一个命名空间一样。就比如在一个Stack.h文件中的命名空间dj中定义了一个栈又在另一个Queue.h文件中定义了一个dj命名空间在这个命名空间中定义了一个队列这两个命名空间会被默认合并。命名空间的使用1.指定命名空间访问#includeiostream using namespace std; namespace N { int a 0; } int main() { coutN::aendl; return 0; }2.using将命名空间中某个成员展开using N::b;这种适用于需要被频繁使用但是不易产生命名冲突的变量等3.展开命名空间中全部成员using namespace N;一般项目中不推荐这种写法易造成名称污染坑点命名空间只能定义在全局不能定义在局部在函数局部域中展开的命名空间只能在函数局部直接使用函数一旦被销毁还是需要用域作用限定符来访问void func1() { using namespace N; }比如这里函数func1一旦被销毁就需要通过访问命名空间中的成员了否则编译器就会报错。2缺省参数核心什么是缺省参数缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时如果没有指定实参则采⽤该形参的缺省值否则使⽤指定的实参缺省参数分为全缺省和半缺省参数。有些地⽅把 缺省参数也叫默认参数#include iostream #include assert.h using namespace std; void Func(int a 0) { cout a endl; } int main() { Func(); // 没有传参时使⽤参数的默认值 Func(10); // 传参时使⽤指定的实参 return 0; }细节全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值。C规定半缺省参数必须从右往左依次连续缺省不能间隔跳跃给缺省值。// 全缺省 void Func1(int a 10, int b 20, int c 30) { cout a a endl; cout b b endl; cout c c endl endl; }// 半缺省 void Func2(int a, int b 10, int c 20) { cout a a endl; cout b b endl; cout c c endl; }坑点函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现规定必须函数声明给缺省值。3函数重载C⽀持在同⼀作⽤域中出现同名函数但是要求这些同名函数的形参不同可以是参数个数不同或者类型不同。int Add(int x,int y) { return xy; } double Add(double x,double y) { return xy; }两个Add函数参数类型不同构成函数重载。坑点返回值不同不能作为函数重载的条件因为调用时无法区分调用哪个函数。全缺省和无参函数构成函数重载时存在调用歧义。void func(int a 1) { coutaendl; } void func() { coutvoid func();endl; }这两个函数在不传参的时候就存在调用歧义。内层同名函数会遮蔽外层的所有重载这里有一个非常反直觉的现象很多人会认为这里会调用参数为int型的f函数但是事实确实调用的char类型参数的f函数。因为编译器在编译阶段查找f的声明时优先在主函数的函数局部域中查找在找到void f(char x); 的声明时就停止了压根没注意到剩下的两个函数重载。4引用从语法层面来讲引⽤不是新定义⼀个变量⽽是给已存在变量取了⼀个别名编译器不会为引⽤变量开辟内存空间 它和它引⽤的变量共⽤同⼀块内存空间。在传参的时候通过传递一个参数的引用能够减少拷贝提高效率。void swap(int a, int b) { int t a; a b; b t; }细节引用不是独立对象sizeof/取地址得到的是原对象int x 10; int r x; sizeof(r); // 等于 sizeof(x)不是引用的大小 r; // 得到的是 x 的地址没有引用自己的地址引用必须初始化且一旦引用一个实体之后就不能引用其他实体int a 1, b 2; int r a; // 必须定义时就初始化 r b; // ❗ 这不是让 r 改绑到 b而是把 b 的值赋给 ar 还是 a 的别名const引用如果一个变量被const修饰要想引用这个变量引用的对象就需要被const修饰否则就会触发权限放大。const int a 10; int b a;//权限放大报错 //正确写法 const int ra a;当然也可以缩小权限int a 10; const int ra a;//ra引用只能查看不能修改a的值const引用还有一个应用场景就是在涉及到表达式求值或者类型转换产生临时变量的场景下int a 3; const int ra 3*a; double b 3.14; const int rb b;这样⼀些场景下a*3的积结果保存在⼀个临时对象中 int rb b 也是类似在类型转换中会产⽣临时对象存储中间值也就是说rb和ra引⽤的都是临时对象⽽C规定临时对象具有常性所以这⾥就触发了权限放⼤必须要⽤常引⽤才可以。此时这些临时变量的生命周期会被延长知道对应的引用被销毁临时变量生命周期结束。所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象 C中把这个未命名对象叫做临时对象。坑点不能返回局部变量的引用int f() { int x 5; return x; // ❌ 危险函数结束后 x 已销毁返回的引用指向已失效内存 }5指针和引用的关系C中指针和引⽤就像两个性格迥异的亲兄弟指针是哥哥引⽤是弟弟在实践中他们相辅相成功能有重叠性但是各有⾃⼰的特点互相不可替代。语法概念上引⽤是⼀个变量的取别名不开空间指针是存储⼀个变量地址要开空间。引⽤在定义时必须初始化指针建议初始化但是语法上不是必须的。引⽤在初始化时引⽤⼀个对象后就不能再引⽤其他对象⽽指针可以在不断地改变指向对象。引⽤可以直接访问指向对象指针需要解引⽤才是访问指向对象。sizeof中含义不同引⽤结果为引⽤类型的⼤⼩但指针始终是地址空间所占字节个数(32位平台下占4个字节64位下占8个字节)指针很容易出现空指针和野指针的问题引⽤很少出现引⽤使⽤起来相对更安全⼀些。6内联函数inline被inline关键字修饰的函数就是内联函数。inline提示编译器在函数调用处展开函数从而减少函数栈栈的建立提高效率减小开销。细节inline对于编译器⽽⾔只是⼀个建议也就是说你加了inline编译器也可以选择在调⽤的地⽅不展开不同编译器关于inline什么情况展开各不相同因为C标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数对于递归函数代码相对多⼀些的函数加上inline也会被编译器忽略。vs编译器 debug版本下⾯默认是不展开inline的这样⽅便调试。类内定义的成员函数默认就是 inlineclass A { public: int get() { return x; } // 类内直接定义自动 inline int x; };inline 取代宏避免宏的副作用#define Add(x,y) ((x)(y)) inline int Add(int x,int y) { return xy; }C设计内联函数主要就是为了替代C语言的宏函数的因为C语言的宏函数虽然也能减少开销但是太过复杂还不易调试。就比如这个加法宏需要加三个括号x和y的这两个括号主要是为了处理运算符优先级的问题如果传的参数中的运算符优先级低于结果就会和预期不同。外面的括号也是为了解决运算符优先级的问题防止优先级低于外部运算符产生问题。坑点inline不建议声明和定义分离到两个⽂件inline 函数的定义必须放在头文件里可见分离会导致链接错误。7nullptrnullptr是 C11 的空指针字面量类型明确不是整数细节为什么不用C语言中的NULL而重新设计了nullptr#includeiostream 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语言中NULL本质是一个宏可能是0也可能是(void*)0在涉及到函数重载传参的场景中经常会出现重载歧义。本想通过f(NULL)调⽤指针版本的f(int*)函数但是由于NULL被定义成0调⽤了f(int x)因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。为了解决这个问题C11中引⼊nullptrnullptr是⼀个特殊的关键字nullptr是⼀种特殊类型的字⾯量它可以转换 成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题因为nullptr只能被隐式地转换为指针类型⽽不能被转换为整数类型。写在最后回头看这 7 个知识点它们其实都在回答同一件事C 把 C 里那些「靠人肉小心」的地方一个个交还给了编译器和类型系统——名字会撞 → 命名空间帮你隔离指针又丑又复杂→ 引用给你「传值的写法、传址的效果」宏没类型检查 → 内联函数替你兜底NULL身份不清 →nullptr一锤定音。3 道自测题检验你是否真看懂了Q1下面代码输出什么为什么void f(int) { cout 1; } void f(double) { cout 2; } int main() { void f(char); f(1); return 0; }Q2执行下面三行后a、b的值分别是多少r改绑到b了吗int a 1, b 2; int r a; r b;Q3对于void f(int)和void f(int*)两个重载f(NULL)和f(nullptr)分别调用哪个答案 Q1输出3写 char 的话就是对应字符——main 里的void f(char)把全局两个重载全部遮蔽编译器的名字查找在局部域「找到即停」1被标准转换成 char只调局部版本。 Q2a 2b 2。r b是赋值把 b 的值写进 r 绑定的 a不是改绑——引用一旦绑定终身不变。 Q3f(NULL)调f(int)NULL 本质是 0f(nullptr)调f(int*)——这正是 nullptr 存在的根本理由。下篇预告C 的重头戏终于要来了——类和对象类是怎么把数据和函数「装」在一起的C 写的 Stack 和 C 写的 Stack 到底差在哪我们下篇见。
返回列表