学完 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 school { namespace class1 { int a = 1; } namespace class2 { int a = 2; } }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; }细节
// 全缺省 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:函数重载
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;坑点
int& f() { int x = 5; return x; // ❌ 危险!函数结束后 x 已销毁,返回的引用指向已失效内存 }5:指针和引用的关系
- 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
- 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
- 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
- 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的这两个括号主要是为了处理运算符优先级的问题,如果传的参数中的运算符优先级低于+结果就会和预期不同。外面的括号也是为了解决运算符优先级的问题防止+优先级低于外部运算符产生问题。
坑点
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; }写在最后
回头看这 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,只调局部版本。 Q2:a = 2,b = 2。r = b是赋值(把 b 的值写进 r 绑定的 a),不是改绑——引用一旦绑定终身不变。 Q3:f(NULL)调f(int)(NULL 本质是 0),f(nullptr)调f(int*)——这正是 nullptr 存在的根本理由。
下篇预告
C++ 的重头戏终于要来了——类和对象:类是怎么把数据和函数「装」在一起的?C 写的 Stack 和 C++ 写的 Stack 到底差在哪?我们下篇见。