目录
[2.1 namespace](#2.1 namespace)
[2.2 作用域操作符 ::](#2.2 作用域操作符 ::)
[2.3 命名空间的使用](#2.3 命名空间的使用)
[6.1 定义](#6.1 定义)
[6.2 特性](#6.2 特性)
[6.3 使用](#6.3 使用)
[6.4 const引用](#6.4 const引用)
[6.5 指针和引用的关系](#6.5 指针和引用的关系)
此处省略C++的发展历史......
绪论:C++版本更新


C++从多年一更变成了固定的三年一更,中间经历了许多的插曲,最后思路转变成从 "等所有东西都完美了再发布" 变成 "到点就发,好的坏的都按班车走"。这就是现在能看C++11/14/17/20/23/26 这样整齐的 3 年节奏。
C++参考文档:
--- 非官方,只到 C++11,但按头文件组织、好懂,适合入门查用法
--- 官方中文版,信息全、更新到最新标准,但偏硬核
--- 官方英文版,最权威,内容同中文版
总结:入门查用法看第一个,查最新标准细节看后两个,结合着用。
一、C++的第一个程序
C++兼容C语言绝大多数的语法,所以C语言实现的Hello World依旧可以运行,C++需要把定义的文件后缀改为 .cpp ,vs编译器看到 .cpp 就会调用C++编译器编译。其他平台也有相关的C++编译器,比如Linux下是g++。
cpp
// C++代码
#include<iostream>
using namespace std;
int main() {
cout << "Hello World!" << endl;
return 0;
}
二、命名空间
2.1 namespace
本质是圈出一个独立作用域,解决同名冲突。
语法:
cpp
namespace 名字 { 变量/函数/类型 }
规则:
1、C++有四种域:函数局部域、全局域、命名空间域、类域,都用于编译时名字查找。
2、局部域/全局域会影响变量声明周期;命名空间域/类域不影响的生命周期。
3、namespace只能写在全局,可以嵌套;多文件里同名namespace自动合并,不冲突。
4、C++标准库全部放在std命名空间里。
总结:namespace 就是给变量 / 函数套个 "家族姓",避免不同模块撞名;只管编译时叫谁,不管变量活多久。
以下C语言代码会报错:
cpp
#include<stdio.h>
#include<stdlib.h>
int main() {
int rand = 9;
printf("%d", rand);
return 0;
}
报错如下,"找到一个或多个重定义的符号",主要原因是stdlib标准库中已经有一个名为 rand 的函数,此时在C语言这里再次定义一个整型变量命名为rand就会报错,需要C++中namespace来解决

C++解决代码如下:
cpp
#include<stdio.h>
#include<stdlib.h>
namespace xys {
int rand = 9;
// 函数
int add(int a, int b) {
return a + b;
}
// 类型
struct Stu {
char name;
int age;
};
}
int main() {
printf("%d", xys::rand);
return 0;
}
利用namespace来声明了一个域,把整型变量rand放在改域里就不会报错了,同时也可以在域中可以定义变量、函数、类型。
细心的博友们已经在这里发现了我在问namespace中的rand时,加上了前缀 xys:: ,那么接下来我来详细讲解为什么加上此前缀,以及新的操作符。
2.2 作用域操作符 ::
作用:明确指定标识符所属的作用域,解决命名冲突、访问被内层作用域屏蔽的外层实体。
- 访问全局作用域
当局部变量与全局变量命名冲突时,局部变量会屏蔽全局变量;在变量名前加 :: 即课显示访问全局作用域的实体。
cpp
int num = 100; // 全局变量
int main() {
int num = 10; // 局部变量,屏蔽了全局num
cout << num; // 输出 10(局部变量)
cout << ::num; // 输出 100(全局变量)
}
- 访问命名空间成员
cpp
std::cout << "hello"; // 访问 std 命名空间中的 cout
std::string str; // 访问 std 命名空间中的 string
嵌套命名空间可逐层使用 :: 访问:
cpp
namespace A {
namespace B { int val = 5; }
}
int x = A::B::val; // 访问嵌套命名空间的成员
- 访问类的成员
在类外部定义成员函数时,必须用 类名::函数名指明该函数的所属类。
访问类的静态成员(静态变量/静态函数)时,直接通过 类名 :: 访问,无需实例化对象。
cpp
class Student {
public:
static int count; // 静态成员声明
void show(); // 成员函数声明
};
// 类外定义成员函数
void Student::show() { /* ... */ }
// 类外初始化静态成员
int Student::count = 0;
int main() {
int n = Student::count; // 直接通过类名访问静态成员
}
2.3 命名空间的使用
编译器默认仅在局部和全局作用域查找标识符,不会自动检索命名空间,直接访问命名空间内的成员会编译报错。使用命名空间成员有三种方式:
1、指定命名空间访问 (加::前缀):项目开发推荐使用
cpp
namespace N {
int apple = 3;
int banana = 5;
}
int main{
std::cout << N::apple << std::endl;
return 0;
}
通过 命名空间名 :: 成员名 明确指定作用域,无命名冲突风险,我们常用的 std::out就属于这种方式。
2、using 展开单个成员:适合频繁访问且无命名冲突的成员
cpp
namespace N {
int apple = 3;
int banana = 5;
}
using N::apple; // 只展开命名空间N中的成员apple
int main{
std::cout << apple << std::endl; // apple 前不用再加上 N::
return 0;
}
仅展开指定的成员,该成员可直接使用;未展开的成员仍需加命名空间前缀,适合频繁访问且无冲突的成员。
3、展开全部命名空间成员:仅适合小型练习代码,项目中冲突风险高,不推荐
cpp
namespace N {
int apple = 3;
int banana = 5;
}
using namespace N; // 展开N命名空间的所有成员
int main{
std::cout << apple << std::endl; // apple 前不用加上 N::
std::cout << banana << std::endl; // banana 前不用加上 N::
return 0;
}
using namespace 命名空间名 会把该命名空间所有成员全部引入当前作用域,写法简便但易引发命名冲突,大型项目不推荐。
我们日常练习代码里常见 using namespace std; 就是第三种用法,可以省略 **std::**前缀直接写cout、string,但正式项目中一般不这么写。
三、C++输入&输出
1、 <iostream> 是标准输入输出流库,提供 std::cin (标准输入流对象)、std::out (标准输出流对象);std::endl是操纵符,作用为换行并刷新输出缓冲区。
2、通过 << (流插入 / 输出)、>> (流提取 / 输入)运算符完成 IO 操作,依靠函数重载自动识别变量类型,无需手动指定格式,同时支持自定义类型的输入输出。
3、所有 IO 组件都归属于 std 命名空间;练习代码可用 using namespace std; 简化写法,正式项目不推荐。
4、VS 编译器下引入 <iostream> 可间接使用 printf/scanf,但不具备通用性;大数据量输入场景(如算法竞赛),可通过关闭 stdio 同步、解绑 cin 与 cout 来提升 IO 效率。
cpp
#include <iostream>
using namespace std; // 展开std命名空间,仅推荐练习使用
int main()
{
double point = 0.0;
cin >> point;
cout << point << endl;
int juice = 0;
cin >> juice;
cout << juice << endl;
return 0;
}
四、缺省参数
定义:函数声明时给参数一个默认值,调用时不传就自动用这个默认值传进去。
代码示例:
cpp
#include <iostream>
// b和c都有默认值
int sum(int a, int b = 0, int c = 0) {
return a + b + c;
}
using namespace std;
int main() {
cout << sum(1); // 1,相当于sum(1, 0, 0)
cout << sum(1, 2); // 3,相当于sum(1, 2, 0)
cout << sum(1, 2, 3); // 6,全自己传
}
全缺省:所有参数都有默认值
半缺省:只有部分参数有默认值
三条硬规则
1. 半缺省必须从右往左连续给默认值,不能跳着给
cpp
void f(int a, int b = 10, int c); // 错误!b有默认值但c没有,c在b右边
void f(int a, int b, int c = 10); // 正确
void f(int a, int b = 10, int c = 20); // 正确
2. 声明和定义分离时,默认值只写在声明里
cpp
// 头文件里:
void f(int a = 10);
// .cpp里:
void f(int a) { ... } // 不要再写=10,否则重复定义
3. 不能和函数重载产生歧义
cpp
void f(int a = 10) { }
void f() { } // 错误!调用f()时编译器不知道调哪个:
// 是调无参版本,还是调f(10)?
底层原理:缺省参数不是运行时填充的,是编译期在调用点直接把默认值塞进去:
cpp
sum(1);
// 编译后等价于:
sum(1, 0, 0);
五、函数重载
定义:同一个作用域可以定义同名函数,但参数列表必须不同(C语言不支持,C++支持)
三种合法重载形式:
1、参数类型不同:
cpp
Add(int,int);
Add(double,double);
2、参数个数不同:
cpp
Add(int,int);
Add(int,int,int);
3、参数类型顺序不同:
cpp
f(int,char);
f(char,int);
不能算重载的情况
1、返回值不同:调用时编译器只看实参,看不到返回值,无法区分。
2、和缺省参数产生歧义:f() 和 f(int a = 10) 同时存在时,调 f() 编译器不知道该选哪个,产生歧义。
六、引用
6.1 定义
本质:不是新定义变量,而是给已存在的变量取个别名,编译器不为引用分配新内存,它和原变量共用同一块内存空间。
语法:
cpp
类型& 别名 = 原变量;
代码示例:
cpp
#include<iostream>
using namespace std;
int main() {
int a = 10;
int& b = a;
int& c = b;
int& d = c;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
运行结果:

6.2 特性
三个规则:
1、引用在定义时必须初始化。
2、一个变量可以有多个引用。
3、引用一旦绑定就不能改绑。
6.3 使用
引用的使用主要在两部分:引用传参 和 引用返回
引用传参:
类似指针传参,功能相同但时写法更简洁------函数改形参就是改实参,还省去了拷贝。
代码示例:
cpp
void Swap(int& rx, int& ry) {
int tmp = rx;
rx = ry;
ry = tmp;
}
int main() {
int x = 0, y = 9;
Swap(x, y); // 直接传变量,不用传地址
// x变成1,y变成0
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
}
传引用返回:
代替传值返回,避免产生临时对象,还能让返回值当左值使用。

代码示例:
cpp
// 传值返回:返回临时对象,不能做左值
// int STTop(ST& rs) {
// return rs.a[rs.top];
// }
// STTop(st1) += 10; // 编译错误:+=左边必须是左值
// 传引用返回:返回栈顶元素的别名
int& STTop(ST& rs) {
assert(rs.top > 0);
return rs.a[rs.top];
}
int main() {
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
STTop(st1) += 10; // 直接把栈顶元素2改成12
cout << STTop(st1); // 输出12
}
小补充:
左值 vs 右值
一个简单的判断方法
一个表达式,能不能取地址(&)?
能取地址 → 左值
不能取地址 → 右值
| | 左值(lvalue) | 右值(rvalue) |
| 本质 | 有名字、有持久内存的东西 | 临时的、用完即毁的东西 |
| 能不能取地址 | 能(&x合法) | 不能(&5编译错误) |
| 能不能放 = 左边 | 能 | 不能 |
| 能不能放 = 右边 | 能 | 能 |
| 生命周期 | 一直存在,直到离开作用域 | 语句结束就销毁 |
|---|
代替二级指针------链表头插
cpp
typedef struct ListNode {
int val;
struct ListNode* next;
} LTNode, *PNode;
// 用 PNode& phead 代替 LTNode** phead,写法更直观
void ListPushBack(PNode& phead, int x) {
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL) {
phead = newnode; // 直接改phead,不用*phead = newnode
}
}
int main() {
PNode plist = NULL;
ListPushBack(plist, 1); // 直接传plist,不用&plist
}
总结:引用传参就是给实参起个别名进函数用,传引用返回就是给函数内部某个变量起别名带出去------少写指针、少拷贝、还能直接改。
6.4 const引用
const引用规则:权限只能缩小,不能放大。
例如:
普通变量(读写权限)→ const 引用(只读权限):可以(缩小权限)
const 变量(只读权限)→ 普通引用(想读写):不可以(放大权限)
临时对象必须用 const 引用绑定
像 a*3 的结果、double 转 int 产生的中间值,编译器会生成一个临时对象 暂存,这个临时对象自带常性,普通引用绑不上,必须用 const + &
cpp
int main() {
// 1. const对象必须用const引用
const int a = 10;
// int& ra = a; // ✗ 权限放大:a只读,你想通过ra改它
const int& ra = a; // ✓ 正确
// 2. 普通对象可以用const引用(权限缩小)
int b = 20;
const int& rb = b; // ✓ rb只读,但b本身还能改
// rb++; // ✗ rb是const,不能通过rb改
// 3. 临时对象必须用const引用
// int& rc = a * 3; // ✗ a*3是临时对象,常性,普通引用绑不上
const int& rc = a * 3; // ✓
// 4. 类型转换也产生临时对象
double d = 12.34;
// int& rd = d; // ✗ double转int产生临时对象
const int& rd = d; // ✓
}
6.5 指针和引用的关系
| 对比项 | 引用 | 指针 |
| 本质 | 变量别名,不开新内存 | 存储地址的变量,要开空间 |
| 初始化 | 定义时必须初始化 | 建议初始化,语法上不强制 |
| 改指向 | 绑死一个对象后不能再换 | 可以随意改指向 |
| 访问方式 | 直接用,不用解引用 | 要 * 解引用才访问对象 |
| sizeof | 结果是引用类型本身的大小 | 永远是地址大小(32 位 4 字节,64 位 8 字节) |
| 安全性 | 很少出问题,相对安全 | 容易出现空指针、野指针 |
|---|
总结:引用是更安全、更简洁的 "别名",但不能改绑、不能空;指针更灵活但要自己管地址、容易出错。实践中该用哪个用哪个,不能互相替代。
七、inline函数
为什么要有inline函数?
C 语言的宏函数在预处理时直接文本替换,复杂宏容易出错、不好调试;C++ 设计 inline 就是为了替代宏函数,解决宏的问题。
inline函数能做什么?
建议编译器在调用点把函数体直接展开,省去函数调用的压栈 / 跳转 / 返回开销(和宏效果类似,但它是真正的函数,有类型检查、作用域)
注意:
**1、**inline 只是给编译器的建议,不是强制;函数太复杂(递归、循环太多)编译器不会展开。
2、vs 调试:debug 版本默认不展开 inline(方便打断点调试),release 版本才展开;想在 debug 里展开要改项目设置。
3、关键规则 :inline 的声明和定义必须放同一个文件(一般直接写在头文件里),不能分离到.h 和.cpp,否则链接错误。
cpp
#include <iostream>
using namespace std;
// 直接写在头文件/同一个文件里
inline int Add(int x, int y) {
return x + y;
}
int main() {
int ret = Add(8, 9) * 3;
cout << ret << endl;
}
对比宏函数的坑
cpp
// 宏的问题:不加括号会出bug
// #define ADD(a, b) a + b // 错误:ADD(1,2)*5 会变成 1+2*5=11
#define ADD(a, b) ((a) + (b)) // 正确:外层括号+内层括号都要加
int main() {
cout << ADD(1, 2) * 5 << endl; // ((1)+(2))*5 = 15
}
错误用法:
声明定义分离导致链接错误
cpp
// F.h
#include <iostream>
using namespace std;
inline void f(int i); // 只声明
// F.cpp
#include "F.h"
void f(int i) { // 定义
cout << i << endl;
}
// main.cpp
#include "F.h"
int main() {
f(10); // 链接错误!找不到f的地址(inline被展开后没有独立函数地址)
}
总结:inline 就是 "带类型检查的宏"------ 写在头文件里,建议编译器把函数体直接塞进调用点,既快又安全;千万别把声明和定义分到两个.cpp 文件里,否则链接报错。
八、nullptr
定义:nullptr是C++11引入的关键字 ,专门用来表示空指针。它不是0,不是NULL,是一个独立类型 nullptr_t 的字面值。
cpp
// 头文件里定义(不用自己写)
typedef decltype(nullptr) nullptr_t;
注意:nullptr 只能隐式转换成指针类型,不能转成整数。
什么是"隐式转换"?
隐式转换 = 编译器自动帮你转,你不用写任何代码。
显式转换 = 你自己写强转,比如 (int*)NULL 。
问题:在C++中,NULL就是 0
cpp
void f(int x) {
cout << "f(int)";
}
void f(int* ptr) {
cout << "f(int*)";
}
f(0); // 调 f(int)
f(NULL); // 还是调 f(int)!因为NULL在C++里被定义成0,不是指针
f((int*)NULL); // 手动强转才能调到指针版本
f(nullptr); // ✓ 明确调用 f(int*),不会有歧义
总结:以前用 NULL 表示空指针,但 NULL 在 C++ 里就是 0,函数重载时会误调到 int 版本;C++11 用 nullptr 代替它,专门表示空指针,不会和整数 0 混淆。
最后,感谢你的阅读,欢迎交流与指正!
下一篇我们再见🔜,Bye bye 👋