学过 C 语言再来看 C++,会发现很多东西"似曾相识但又不一样"。这篇博客把入门必须掌握的核心语法都整理清楚,重点标出和 C 的区别,方便复习和练习。
目录
[1. 命名空间 namespace](#1. 命名空间 namespace)
[2. C++ 输入输出](#2. C++ 输入输出)
[3. 缺省参数(默认参数)](#3. 缺省参数(默认参数))
[全缺省 vs 半缺省](#全缺省 vs 半缺省)
[4. 函数重载](#4. 函数重载)
[5. 引用](#5. 引用)
[const 引用](#const 引用)
[引用 vs 指针](#引用 vs 指针)
[6. inline 内联函数](#6. inline 内联函数)
[inline vs 宏函数](#inline vs 宏函数)
[7. nullptr](#7. nullptr)
[C++11 引入 nullptr](#C++11 引入 nullptr)
1. 命名空间 namespace
为什么需要它?
C 语言里所有全局变量、函数名都堆在一起,项目大了极容易冲突。比如你自己定义了一个 rand 变量,结果和标准库的 rand() 函数撞名,直接报错:
#include <stdlib.h>
int rand = 10; // 编译报错:rand 重定义,stdlib 里有个 rand() 函数
C++ 用 namespace 解决这个问题------把名字关进一个独立的域里,和全局域互不干扰。
定义方式
cpp
namespace TomGo // TomGo 是命名空间名,一般用项目名
{
int rand = 10; // 变量
int Add(int a, int b) // 函数
{
return a + b;
}
struct Node // 类型
{
int val;
Node* next;
};
}
几个要记住的规则:
- namespace 只能定义在全局(不能在函数内部)
- 可以嵌套定义
- 多个文件里同名的 namespace 会自动合并,不冲突
- C++ 标准库全部放在
std这个命名空间里
嵌套命名空间
cpp
namespace TomGo
{
namespace zjy // 嵌套
{
int val = 1;
}
namespace zxy
{
int val = 2;
}
}
// 访问时逐层展开
TomGo::zjy::val; // 1
TomGo::zxy::val; // 2
三种使用方式
编译器默认只在局部域和全局域查找,不会自动进命名空间找。所以要用命名空间里的东西,有三种方式:
html
namespace N
{
int a = 0;
int b = 1;
}
// 方式一:指定命名空间访问(项目开发推荐)
printf("%d\n", N::a);
// 方式二:using 展开单个成员(常用成员且不冲突时推荐)
using N::b;
printf("%d\n", b); // 直接用 b
// 方式三:展开整个命名空间(小练习方便,项目不推荐,风险高)
using namespace N;
printf("%d\n", a);
printf("%d\n", b);
实践建议 :
using namespace std;在练习中随便用,正式项目里要慎用,因为 std 里名字太多了,容易和自己的代码冲突。
2. C++ 输入输出
C++ 有自己的一套 IO,用 <iostream> 头文件。
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
// 输出:<< 是流插入运算符
cout << a << " " << b << " " << c << endl;
// 输入:>> 是流提取运算符
cin >> a;
cin >> b >> c; // 可以连着写
return 0;
}
和 printf/scanf 的对比:
| C 的 printf/scanf | C++ 的 cout/cin | |
|---|---|---|
| 格式串 | 必须手动写 %d %lf |
自动识别类型,不用写 |
| 扩展性 | 只支持内置类型 | 支持自定义类型(通过运算符重载) |
| 效率 | 较快 | 默认有同步开销,竞赛可关闭 |
关键点:
cout是输出流对象,cin是输入流对象,都在std命名空间里endl= 输出换行 + 刷新缓冲区(等价于'\n'+ flush,如果不需要刷新用'\n'更快)<<和>>在 C 里是位运算符,C++ 里复用了这两个符号做 IO
竞赛提速(IO 量大时加这三行):
ios_base::sync_with_stdio(false); // 关闭 C/C++ IO 同步
cin.tie(nullptr); // 解除 cin 和 cout 的绑定
cout.tie(nullptr);
加了这三行后,不要再混用 printf/scanf,否则输出顺序可能乱。
3. 缺省参数(默认参数)
函数定义时给参数一个默认值,调用时可以不传。
cpp
void Func(int a = 0)
{
cout << a << endl;
}
Func(); // 不传,a = 0
Func(10); // 传了,a = 10
全缺省 vs 半缺省
cpp
// 全缺省:所有参数都有默认值
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << a << " " << b << " " << c << endl;
}
Func1(); // 10 20 30
Func1(1); // 1 20 30
Func1(1, 2); // 1 2 30
Func1(1, 2, 3); // 1 2 3
// 半缺省:只有部分参数有默认值,必须从右往左连续给
void Func2(int a, int b = 10, int c = 20) // a 没有默认值
{
cout << a << " " << b << " " << c << endl;
}
// 错误写法(中间不能跳):void Func(int a = 1, int b, int c = 3)
调用规则:必须从左往右依次传实参,不能跳着传。
声明和定义分离时
cpp
// Stack.h(头文件里写缺省值)
void STInit(ST* ps, int n = 4);
// Stack.cpp(定义里不写缺省值)
void STInit(ST* ps, int n)
{
// ...
}
规则 :缺省值只能写一次,约定写在声明处。声明和定义同时写会报错。
缺省参数的实际价值:比如初始化栈,不知道容量就用默认 4,知道容量就提前分配好,避免扩容开销。
4. 函数重载
C++ 允许在同一作用域里有多个同名函数,只要参数不同(类型、个数、顺序)就行。C 语言不支持这个。
// 参数类型不同
int Add(int a, int b) { return a + b; }
double Add(double a, double b) { return a + b; }
// 参数个数不同
void f() {}
void f(int a) {}
// 参数顺序不同
void f(int a, char b) {}
void f(char b, int a) {}
int main()
{
Add(1, 2); // 调用 int 版本
Add(1.1, 2.2); // 调用 double 版本
f(); // 无参版本
f(10, 'a'); // int, char 版本
f('a', 10); // char, int 版本
}
不能作为重载条件的情况:
bash
// 仅返回值不同------不构成重载,调用时无法区分
int fxx() { return 0; }
void fxx() {} // 报错
// 这两个虽然构成重载,但调用 f1() 时有歧义(编译器不知道调哪个)
void f1() {}
void f1(int a = 10) {} // f1() 两个都能匹配,调用时报歧义错误
重载的底层原理 :C++ 编译器会对函数名做"名字修饰"(Name Mangling),把参数类型信息编码进符号名里,所以 Add(int, int) 和 Add(double, double) 在底层是不同的符号。C 语言不做这个,所以不支持重载。
5. 引用
概念
引用是给已有变量取一个别名,不开新内存,和原变量共用同一块空间。
cpp
int a = 0;
int& b = a; // b 是 a 的别名
int& c = a; // c 也是 a 的别名
int& d = b; // 给别名再取别名,d 还是 a 的别名
++d; // 实际上是 ++a
// 四个地址完全相同
cout << &a << &b << &c << &d << endl;
语法:类型& 引用名 = 被引用的变量;
注意:
&在这里是引用符,不是取地址符。区分方法:出现在类型旁边是引用,出现在变量旁边是取地址。
引用的三条特性
// 1. 定义时必须初始化
int& ra; // 报错!必须初始化
// 2. 一个变量可以有多个引用(见上面)
// 3. 引用一旦绑定,不能改变指向
int a = 10, c = 20;
int& b = a;
b = c; // 这不是让 b 指向 c,而是把 c 的值赋给 a
// b 和 a 的地址仍然相同
引用的主要用途
① 引用传参------替代指针传参,更简洁
// C 的做法(指针)
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
// C++ 的做法(引用)
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int x = 0, y = 1;
Swap(x, y); // 不需要传地址,更直观
还有一个实用场景:替代二级指针
html
// C 里修改链表头节点需要二级指针
void ListPushBack(LTNode** phead, int x) { ... }
// C++ 里用引用替代,简洁得多
void ListPushBack(LTNode*& phead, int x) { ... } // 指针变量的引用
② 引用返回值------直接操作数据,避免拷贝
html
int& STTop(Stack& st) // 返回栈顶元素的引用
{
return st._a[st._top - 1]; // 返回元素本身,不是拷贝
}
// 这样可以直接修改栈顶元素
STTop(st) += 10; // 等价于 st._a[top-1] += 10
⚠️ 危险陷阱:不要返回局部变量的引用!局部变量函数结束就销毁了,引用会变成悬垂引用(相当于野指针)。
const 引用
const int a = 10;
// int& ra = a; // 报错!权限放大,a 是只读的,ra 却能修改
const int& ra = a; // 正确,权限缩小或不变都可以
int b = 20;
const int& rb = b; // 正确,权限缩小(b 可读写,rb 只读)
必须用 const 引用的特殊情况------临时对象:
int a = 10;
// int& rb = a * 3; // 报错!a*3 的结果存在临时对象里,临时对象有"常性"
const int& rb = a * 3; // 正确
double d = 12.34;
// int& rd = d; // 报错!类型转换会产生临时对象
const int& rd = d; // 正确
理解"常性" :编译器生成的临时对象(表达式结果、类型转换中间值)默认是
const的,所以引用它必须用const&,否则相当于权限放大。
引用 vs 指针
| 对比点 | 引用 | 指针 |
|---|---|---|
| 内存 | 不开新空间(语法层面) | 开空间存地址 |
| 初始化 | 必须 | 建议但非强制 |
| 能否改变指向 | 不能 | 能 |
| 访问 | 直接访问 | 需要解引用 * |
| sizeof | 引用对象的大小 | 始终是 4 或 8 字节 |
| 安全性 | 较安全,少有空引用 | 容易出现空指针、野指针 |
两者不是替代关系,各有适合的场景,实践中配合使用。
6. inline 内联函数
是什么
用 inline 修饰的函数,编译器会在调用处直接展开函数体,不走函数调用(不建立栈帧),从而提高效率。
cpp
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(1, 2); // 编译后相当于 int ret = 1 + 2;(直接展开)
}
inline vs 宏函数
C 语言用宏来做类似的事,但宏很容易出错:
html
// 错误的宏实现(常见问题)
#define ADD(a, b) a + b // 错:cout << ADD(1,2)*5 结果是 1+2*5=11,不是 15
#define ADD(a, b) (a + b) // 还是错:ADD(x&y, x|y) -> (x&y+x|y) 优先级问题
// 正确写法(每个参数都加括号,整体也加括号)
#define ADD(a, b) ((a) + (b))
| 对比 | 宏函数 | inline 函数 |
|---|---|---|
| 类型检查 | 无 | 有 |
| 调试 | 困难 | 正常调试 |
| 写法 | 容易出错(括号) | 正常写函数 |
| 展开时机 | 预处理期(文本替换) | 编译期 |
inline 的限制:
-
只是对编译器的建议,编译器可以忽略(递归函数、代码量大的函数一般不会被展开)
-
适合短小、频繁调用的函数
-
不能声明和定义分离到两个文件,因为展开后没有函数地址,链接会找不到符号
// F.h
inline void f(int i); // 声明// F.cpp
void f(int i) { ... } // 定义在另一个文件// main.cpp
f(10); // 链接报错!找不到 f 的定义
解决方式:inline 函数直接把定义写在头文件里。
7. nullptr
问题背景
C 里 NULL 是宏,在 C++ 里被定义为整数 0:
#ifdef __cplusplus
#define NULL 0 // C++ 里是整数 0
#else
#define NULL ((void*)0) // C 里是 void 指针
#endif
这导致一个经典问题:
void f(int x) { cout << "f(int)" << endl; }
void f(int* p) { cout << "f(int*)" << endl; }
f(NULL); // 本想调用指针版本,实际调用了 int 版本!因为 NULL == 0
f((int*)NULL); // 强转才能调用指针版本
C++11 引入 nullptr
nullptr 是专门表示空指针的关键字,类型是 nullptr_t,只能隐式转换为指针类型,不能转为整数。
f(nullptr); // 正确调用 f(int*),没有歧义
规则 :C++ 代码里,初始化空指针一律用 nullptr,不用 NULL。
总结
| 特性 | C | C++ |
|---|---|---|
| 命名空间 | ❌ 没有 | ✅ namespace |
| 输入输出 | printf / scanf | cout / cin(自动识别类型) |
| 默认参数 | ❌ 没有 | ✅ 缺省参数 |
| 函数重载 | ❌ 不支持 | ✅ 支持(名字修饰机制) |
| 引用 | ❌ 没有(只有指针) | ✅ 引用(别名,不开空间) |
| 内联函数 | 宏函数(危险) | inline(安全) |
| 空指针 | NULL(本质是 0) | nullptr(专用关键字) |
练习建议
- 命名空间 :自己写一个
namespace,嵌套两层,练习三种访问方式 - 缺省参数:改写一个 C 的栈初始化函数,加上缺省容量
- 函数重载 :写
Add函数支持 int、double、long long 三种版本 - 引用 :用引用实现
Swap,再尝试返回引用修改数组元素 - const 引用 :试试给常量、临时值(如
a*3)加引用,感受报错信息 - nullptr :写两个重载函数
f(int)和f(int*),分别用NULL和nullptr调用,对比结果