news 2026/9/4 18:36:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《从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.指定命名空间访问

#include<iostream> using namespace std; namespace N { int a = 0; } int main() { cout<<N::a<<endl; 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 x+y; } double Add(double x,double y) { return x+y; }

两个Add函数参数类型不同构成函数重载。

坑点

返回值不同不能作为函数重载的条件,因为调用时无法区分调用哪个函数。

全缺省和无参函数构成函数重载时存在调用歧义。

void func(int a = 1) { cout<<a<<endl; } void func() { cout<<"void 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 的值赋给 a(r 还是 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的,这样⽅便调试。
  • 类内定义的成员函数默认就是 inline
class A { public: int get() { return x; } // 类内直接定义,自动 inline int x; };
  • inline 取代宏,避免宏的副作用
#define Add(x,y) ((x)+(y)) inline int Add(int x,int y) { return x+y; }

C++设计内联函数主要就是为了替代C语言的宏函数的,因为C语言的宏函数虽然也能减少开销,但是太过复杂还不易调试。

就比如这个加法宏,需要加三个括号,x和y的这两个括号主要是为了处理运算符优先级的问题,如果传的参数中的运算符优先级低于+结果就会和预期不同。外面的括号也是为了解决运算符优先级的问题防止+优先级低于外部运算符产生问题。

坑点

inline不建议声明和定义分离到两个⽂件,inline 函数的定义必须放在头文件里可见,分离会导致链接错误。

7:nullptr

nullptr是 C++11 的空指针字面量,类型明确,不是整数

细节

为什么不用C语言中的NULL而重新设计了nullptr

#include<iostream> 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);调⽤会报错
为了解决这个问题,C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,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:执行下面三行后,ab的值分别是多少?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,只调局部版本。 Q2:a = 2b = 2r = b赋值(把 b 的值写进 r 绑定的 a),不是改绑——引用一旦绑定终身不变。 Q3:f(NULL)f(int)(NULL 本质是 0),f(nullptr)f(int*)——这正是 nullptr 存在的根本理由。

下篇预告

C++ 的重头戏终于要来了——类和对象:类是怎么把数据和函数「装」在一起的?C 写的 Stack 和 C++ 写的 Stack 到底差在哪?我们下篇见。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 18:35:53

n型BC太阳电池温度衰减与LeTID动力学研究

交指式背接触&#xff08;IBC&#xff09;太阳电池把两种极性的金属接触全部移到背面&#xff0c;正面没有栅线遮光&#xff0c;绒面和减反射膜的光学优势得以充分发挥&#xff0c;配合钝化与接触工艺&#xff0c;n型IBC可实现很高的开路电压和电流密度。代价是结构上的先天敏感…

作者头像 李华
网站建设 2026/9/4 18:35:49

百亿交易、618日9亿:16年金融架构老兵的踩坑全记录

CSDN 技术博客 架构实战 | 16年金融系统经验 | 500人产研团队 | 百亿交易规模 | 618日交易9亿 | JEECG 5K Star写在前面&#xff1a;这不是概念科普CSDN 上讲微服务、讲架构演进的文章很多&#xff0c;但大部分有个共同问题——距离生产太远。讲概念的多&#xff0c;讲踩坑的少…

作者头像 李华
网站建设 2026/9/4 18:31:01

魔兽争霸2实体盘兼容性解决方案与DOSBox配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 18:30:50

从零构建老虎图像识别系统:细粒度分类与长尾数据实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 18:28:24

Claude Code 从零部署与实战:AI编程助手集成VSCode全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华