欢迎阅读本篇学习笔记。
本篇作为个人计算机专业的学习记录,这里将系统梳理关于C++入门的相关知识点,从基础概念到代码实现逐步展开,已便于后续的复习巩固。如有不足,欢迎大家在评论区交流指正,感谢大家的阅读与支持!
一、C++ 整体学习路线
C++ 的学习可以拆成三大核心板块:C++ 基础语法、STL 标准库、高阶数据结构。 基础语法是地基,用来理解面向对象、内存管理、重载、继承多态这些核心思想;STL 是工程开发利器,封装好容器、算法、迭代器,告别重复造轮子;高阶数据结构则是结合 C++ 特性,实现更加高效复杂的数据组织方式,也是笔试面试高频考点。三者层层递进,是学好 C++ 的完整路径。
二、C++ 发展历史
C++ 的设计者是本贾尼・斯特劳斯特卢普(Bjarne Stroustrup),最早在 1979 年于贝尔实验室启动相关研究。当时 C 语言以高效简洁著称,但面向大型复杂项目、系统仿真、操作系统开发时,缺少对代码封装、模块复用的支持,项目规模变大之后维护成本很高。 他的思路不是从零造一门新语言,而是保留 C 语言高性能、贴近底层的全部优势,在此之上增加面向对象能力。1983 年,这门语言正式命名为 C++,class、封装、继承这些核心概念就此落地。 之后多年,C++ 慢慢走进高校课堂,也被大量工业项目选用。模板、泛型编程的思想也在这个阶段慢慢成熟。 标准化工作从 1989 年启动,由 ANSI 和 ISO 联合委员会负责。大名鼎鼎的 STL 标准模板库由惠普实验室团队开发,它的加入极大改变了 C++ 的开发方式,不过因为标准委员会需要仔细评估 STL 的规范,也推迟了标准定稿时间。1998 年,C++98 正式发布,这是 C++ 历史上第一个国际标准。
2.1 C++ 版本迭代
| 时间 | 版本 | 核心亮点 |
|---|---|---|
| 1998 | C++98 | 首个正式标准,引入 STL,模板、异常、命名空间落地 |
| 2003 | C++03 | 修复 C++98 漏洞,侧重规范和稳定性,补充 TR1 库 |
| 2011 | C++11 | 划时代大更新,lambda 表达式、范围 for 循环、右值引用、移动语义、智能指针、线程库,现代 C++ 的起点 |
| 2014 | C++14 | C++11 的小幅补强,泛型 lambda、自动返回值推导、二进制字面量 |
| 2017 | C++17 | if constexpr、折叠表达式,文件系统库、string 优化,简化模板编程 |
| 2020 | C++20 | 又一个里程碑,协程、Concepts 约束、模块 Module、Ranges 范围库 |
| 2023 | C++23 | 进一步简化语法,flat_map、import std、if consteval 等 |
| 未来 | C++26 | 还在制定中,社区期待网络库等重磅特性 |
简单来说:C++11 是分水岭,在它之前是传统 C++,C++11 及以后我们一般叫现代 C++。
2.2 一个有趣的社区小故事
很多 C++ 开发者心心念念标准网络库 networking,它曾经被规划进 C++23,大家都十分期待。但在标准讨论阶段,因为委员会内部多方意见分歧、实现方案争论不休,这个特性最终没能进入 C++23。 这件事在 C++ 社区引发了很长一段时间的热烈讨论和玩梗。也能侧面看出:C++ 标准的推进十分严谨,一个特性想要纳入标准,需要兼顾性能、兼容性、跨平台,门槛极高。
三、C++ 的行业价值
3.1 TIOBE 编程语言排行榜
TIOBE 榜单会综合全网教程、开发者讨论、搜索引擎检索热度等数据,统计各编程语言的流行程度。需要注意,这个榜单只反映语言的社区热度,热度高不代表语言绝对更好,不能单靠排名评判一门语言的优劣。
从 2024 年 6 月这份榜单可以看到,C++ 稳定稳居第二,长期处在第一梯队。Python 凭借入门简单、生态丰富常年领跑,而 C、C++ 这类偏底层、高性能的语言依旧牢牢占据高位。纵观多年的趋势曲线,C++ 的生命力十分顽强,即便不断有新语言诞生,它的市场底盘一直很稳固。
3.2 C++ 在工作领域中的应用
C++ 凭借高性能、内存可控的优势,落地场景非常广泛,覆盖系统底层、音视频、游戏、服务端、嵌入式、AI 底层等方向。下面分场景介绍:
- 大型系统软件开发 编译器、数据库内核、操作系统内核、浏览器底层等基础软件,追求极致运行效率,大多采用 C++ 开发,是整个软件生态的基石。
- 音视频处理 我们熟知的 FFmpeg、WebRTC、MediaSoup、ijkplayer 这些主流开源音视频框架底层都是 C++。直播、短视频、实时通话的音视频编解码,核心技术栈离不开 C++。
- PC 客户端开发 Windows 桌面软件很多基于 C++ 搭配 QT 开发,QT 是跨平台 GUI 框架,可以一套代码适配多系统,典型例子如 WPS 这类大型桌面应用。
- 高性能服务端开发 Java 在后端生态中应用很广,而 C++ 多用于对延迟、吞吐要求严苛的高并发场景,比如游戏后台、流媒体服务器、金融量化高频交易系统。
- 游戏引擎开发 主流商业与开源游戏引擎(UE4、Cocos2d-x 等)底层都是 C++。做引擎或者游戏客户端,除 C++ 语法之外,还需要掌握数据结构、计算机图形学等知识。
- 嵌入式开发 把主控芯片嵌入硬件设备,依靠软件控制硬件。像摄像头、扫地机器人、车载系统、智能手环、门禁设备都属于这个范畴,分为嵌入式应用开发与驱动开发两大方向。
- 机器学习引擎 很多 AI 框架底层算子、推理内核是 C++ 实现,上层再用 Python 封装接口。只做模型训练,Python 就够用;如果要做推理引擎、优化底层算子,C++ 是必备技能。
- 测试开发 测试开发工程师需要自研性能测试工具、自动化框架,很多高性能测试组件会用 C++ 实现;而功能测试更多侧重业务用例,不一定需要 C++。
四、C++ 学习难度
经常有人问:C++ 难不难?客观来说,C++ 属于上手门槛高、易学难精的语言,学习曲线很陡峭。一方面是它背负了 C 语言的历史包袱,另一方面语言本身经过多年迭代,特性繁多、规则复杂。
网上流传着一个很出名的梗:21 天自学精通 C++。这其实是一个搞笑漫画,用来调侃 C++ 很难,21 天根本不可能学明白。 漫画的趣味解读:

- 第 1–10 天:啃基础语法,变量、数组、函数、字符串这些入门内容
- 第 11–21 天:进入 C++ 核心难点,指针、引用、类、继承、多态
- 第 22–697 天:不断写小项目,读别人代码,在 bug 和失败里积累经验
- 第 698–3648 天:参与团队项目,和同行交流,在实战中成长
- 后面的阶段就是玩梗了:学理论物理、生物化学,最后造时光机回到第 21 天,才能 “精通 C++”
这个段子的内核是提醒我们:不存在短期速成精通 C++ 的捷径。语法只是起点,真正吃透内存模型、泛型、STL、工程实践需要长期沉淀。
五 、C++ 的第一个程序
C++ 兼容 C 语言绝大多数语法,所以 C 语言实现的 hello world 依旧可以运行。C++ 源文件后缀改为.cpp;VS 编译器识别到.cpp就调用 C++ 编译器编译,Linux 下编译要用g++,不再是 gcc。
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
当然 C++ 有一套自己的输入输出,严格说 C++ 版本的 hello world 应该是这样写的:
// test.cpp
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
六、 命名空间
6.1 namespace 的价值
在 C/C++ 中,项目里会定义大量变量、函数,后续我们还会学习类。如果所有变量、函数、类的名称全都放在全局作用域下,很容易出现名字冲突。命名空间的作用就是把标识符的名称做区域隔离,以此避免命名冲突、解决名字污染问题,namespace关键字就是专门用来处理这类问题的。
在 C 语言开发中,这种命名冲突的情况十分常见。C++ 引入namespace,就是为了更好地解决名字污染这个痛点。
// 引入标准输入输出头文件,里面包含printf函数的声明
#include <stdio.h>
// 引入标准库头文件,C标准库在这里声明了rand()随机数函数
#include <stdlib.h>
// 这里定义了一个全局变量,名字叫rand
int rand = 10;
int main()
{
// 编译报错: error C2365: “rand”: 重定义; 以前的定义是“函数”
/*
报错原因解析:
1. #include <stdlib.h> 已经帮我们声明了库函数 rand(),rand 是一个函数名;
2. 我们自己又定义了全局变量 int rand;,变量名和库函数名字完全一样;
3. C语言中,同一个作用域下,标识符(名字)不能同时代表两个东西,
编译器分不清你这里的rand到底是库函数,还是你自己定义的变量,触发重定义错误。
✅ 简单一句话:变量名和库rand函数名字冲突了!
*/
printf("%d\n", rand);
return 0;
}
6.2 namespace 的定义
-
定义命名空间需要使用
namespace关键字,后面跟上命名空间的名称,再紧跟一对花括号{},花括号内部的内容就是该命名空间的成员。命名空间中可以定义变量、函数、自定义类型等多种成员。 -
命名空间的本质,是定义出一个独立的作用域。这个作用域和全局作用域相互隔离,不同作用域内可以定义同名标识符,因此原本会冲突的
rand名字,放在不同命名空间里就不再冲突。 -
C++ 中常见的作用域有四种:函数局部域、全局域、命名空间域、类域。作用域的核心作用,是决定编译器在编译阶段查找变量、函数、类型声明或定义的查找规则;有了作用域之间的隔离,命名冲突问题就从根源上得到了解决。 其中局部域和全局域除了影响名字查找逻辑,还会决定变量的生命周期;而命名空间域和类域只影响名字查找,不会改变变量的生命周期。
-
命名空间只能定义在全局作用域下,同时支持嵌套定义。 在大型项目的多个文件中,定义同名的命名空间,编译器会将它们自动合并为同一个命名空间,不会产生冲突。
-
我们常用的 C++ 标准库,所有组件都统一放在名为
std(取自 standard)的命名空间中。
①:基础命名空间,解决重名冲突
#include<iostream>
using namespace std;
// 定义命名空间ZhangSan
namespace ZhangSan
{
// 在命名空间ZhangSan内部定义变量score
int score = 10;
}
// 在全局域定义同名变量score,不会和ZhangSan里面的score冲突
int score = 20;
int main()
{
// :: 是域解析符,访问ZhangSan命名空间中的score,输出10
cout << ZhangSan::score << endl;
// 单独::score 代表访问全局域中的score,输出20
cout << ::score << endl;
return 0;
}
:: 叫做域解析符,用来指定去哪个域里面找名字。
②:命名空间中定义函数
#include<iostream>
using namespace std;
// 定义命名空间Student
namespace Student
{
// 在命名空间里面定义ShowInfo函数
void ShowInfo()
{
cout << "这是Student命名空间里的信息打印函数" << endl;
}
}
int main()
{
// 使用域解析符,调用Student空间中的ShowInfo函数
Student::ShowInfo();
return 0;
}
③:命名空间嵌套
#include<iostream>
using namespace std;
// 外层命名空间School
namespace School
{
int grade = 1;
// 在School里面嵌套定义命名空间Class
namespace Class
{
int num = 2;
}
}
int main()
{
// 访问School空间下的变量grade
cout << School::grade << endl;
// 多层嵌套,连续使用域解析符,访问School里面Class空间的num
cout << School::Class::num << endl;
return 0;
}
④:多文件同名 namespace 自动合并
文件 1:test1.cpp
// 文件1中定义命名空间Game
namespace Game
{
int hp = 100;
}
文件 2:test2.cpp
// 文件2中写同名Game,编译后会和test1里面的合并成同一个命名空间
namespace Game
{
void ShowHP()
{
// 这里可以直接使用Game::hp,两个文件属于同一个命名空间
}
}
⑤:区分域对生命周期的影响
#include<iostream>
using namespace std;
namespace Shop
{
// 命名空间域的变量:仅隔离名字,生命周期和全局变量一致,程序结束才销毁
int money = 10;
}
void func()
{
// 局部域变量:只在func函数内部生效,函数执行结束,变量直接销毁
int a = 20;
cout << a << endl;
}
int main()
{
cout << Shop::money << endl;
func();
return 0;
}
小结:局部域、全局域,控制名字查找 + 生命周期;命名空间域、类域,只控制名字查找,不改变生命周期。
6.3 命名空间使用
编译器查找一个变量的声明 / 定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以下面程序会编译报错。所以我们要使用命名空间中定义的变量 / 函数,有三种方式:
- 指定命名空间访问,项目中推荐这种方式。
- using 将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
- 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
#include<stdio.h>
// 定义命名空间Student
namespace Student
{
int chinese = 0;
int math = 1;
}
int main()
{
// 编译报错: error C2065: “chinese”: 未声明的标识符
// printf("%d\n", chinese);
return 0;
}
// 方式1:指定命名空间访问(项目推荐)
int main()
{
// 使用域解析符,明确指定去Student命名空间找chinese
printf("%d\n", Student::chinese);
return 0;
}
// 方式2:using将命名空间中某个成员展开
using Student::math;
int main()
{
printf("%d\n", Student::chinese);
// math已经被using展开,可以直接使用
printf("%d\n", math);
return 0;
}
// 方式3:展开命名空间中全部成员(练习用,工程慎用)
using namespace Student;
int main()
{
printf("%d\n", chinese);
printf("%d\n", math);
return 0;
}
八、C++ 输入 & 输出
<iostream> 是 Input Output Stream 的缩写,也就是 C++ 的标准输入输出流库,其中定义了标准的输入、输出对象。
std::cin是 istream 类的实例对象,对应面向 char 类型窄字符的标准输入流;std::cout是 ostream 类的实例对象,对应面向窄字符的标准输出流;std::endl本质是一个函数,在流插入输出时,会输出一个换行符,同时强制刷新输出缓冲区。
<< 称为流插入运算符,>> 称为流提取运算符。补充说明:在 C 语言中,这两个符号还用作位运算的左移、右移运算符,到了 C++ 里通过运算符重载,赋予了它们 IO 操作的新含义。
相比 C 语言的 printf/scanf,C++ 输入输出使用更便捷:不需要手动书写格式控制符,编译器可以自动识别变量类型,这个特性底层正是通过函数重载实现的。更重要的是,C++ IO 流能够很好地支持自定义类型对象的输入输出,扩展性更强。
IO 流的完整实现涉及类与对象、运算符重载、继承等大量面向对象知识,这些内容后续会逐步展开,所以现阶段先掌握基础用法即可。
cin、cout、endl 都属于 C++ 标准库组件,而 C++ 标准库的全部内容都统一放在名为 std(取自 standard)的命名空间中,因此使用时需要遵循命名空间的访问规则。 日常练习为了编码方便,可以直接 using namespace std;;但在实际项目开发中不建议这么做,避免引发命名冲突。
最后补充一个细节:即使我们没有显式包含 <stdio.h>,只要包含了 <iostream>,在 VS 系列编译器下也能正常使用 printf 和 scanf,这是因为 <iostream> 间接包含了相关头文件。但这种行为不具备通用性,其他编译器可能直接报错,正式代码里建议显式包含所需的头文件。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
// 直接使用cout输出,自动识别类型
cout << a << " " << b << " " << c << endl;
// 不展开std的写法,完整写法
std::cout << a << " " << b << " " << c << std::endl;
// 混用C语言scanf
scanf("%d%lf", &a, &b);
printf("%d %lf\n", a, b);
// cin自动识别变量类型,不用写格式控制符
cin >> a;
cin >> b >> c;
cout << a << endl;
cout << b << " " << c << endl;
return 0;
}
#include<iostream>
using namespace std;
int main()
{
// 在IO需求比较高的地方,如部分大量输入的竞赛题中,加上以下3行代码
// 可以提高C++IO效率,解除cin/cout和C stdio的同步绑定
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
九、 缺省参数
-
缺省参数,指的是在声明或定义函数时,为形参预先指定一个默认值。调用该函数时,如果没有传入对应的实参,就会自动使用这个默认值;如果传入了实参,则优先使用传入的值。缺省参数分为全缺省和半缺省两类,在部分资料中也将其称为默认参数。
-
全缺省,即函数的所有形参都设置了默认值;半缺省,则是仅为部分形参设置默认值。C++ 语法明确规定:半缺省参数必须从右往左依次连续设置,不能间隔跳跃地给默认值。 调用带缺省参数的函数时,实参必须从左到右依次传递,不能跳过前面的形参,直接给后面的形参传值。
-
当函数采用声明与定义分离的写法时,缺省参数不能在声明和定义中同时出现,语法规定默认值必须写在函数声明里。
示例 1:单个缺省参数
#include <iostream>
using namespace std;
// 给形参a设置缺省值0
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 不传实参,使用默认值0
Func(10); // 传入实参,使用10覆盖默认值
return 0;
}
示例 2:全缺省 + 半缺省
#include <iostream>
using namespace std;
// 全缺省:所有参数都给默认值
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << a << " " << b << " " << c << endl;
}
// 半缺省:必须从右往左连续给默认值,不能跳着给
void Func2(int a, int b = 10, int c = 20)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Func1(); // 全部用默认:10 20 30
Func1(1); // a=1,b、c默认:1 20 30
Func1(1,2); // a=1,b=2,c默认:1 2 30
Func2(100); // a=100,b、c默认:100 10 20
Func2(100,200); // a=100,b=200,c默认:100 200 20
Func2(100,200,300); // 全部传参:100 200 300
return 0;
}
示例 3:声明定义分离
#include <iostream>
#include <assert.h>
using namespace std;
// 函数声明:在这里给缺省值
void STInit(int n = 4);
// 函数定义:定义处不能再写默认值!
void STInit(int n)
{
assert(n > 0);
cout << "初始化容量:" << n << endl;
}
int main()
{
STInit(); // 使用默认值4
STInit(1000); // 自己指定容量
return 0;
}
十、 函数重载
C++ 提供了函数重载特性:在同一个作用域中,可以定义多个函数名完全相同的函数。编译器会根据调用时传入实参的个数、类型、类型顺序,自动匹配并选择最合适的函数版本执行,让同名函数具备不同的处理逻辑,代码写起来更加灵活。而 C 语言没有重载机制,同一作用域内不允许出现同名函数,否则直接编译失败。
想要构成合法的函数重载,形参列表必须存在差异,满足下面三种情况之一即可:
① 参数类型不同
#include<iostream>
using namespace std;
// 两个Add同名,参数类型不一样,构成重载
int Add(int left, int right)
{
cout << "int版本Add" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double版本Add" << endl;
return left + right;
}
int main()
{
Add(10, 20); // 匹配int版本
Add(10.1, 20.2); // 匹配double版本
return 0;
}
② 参数个数不同
#include<iostream>
using namespace std;
// 同名f,参数数量不一样,构成重载
void f()
{
cout << "无参 f()" << endl;
}
void f(int a)
{
cout << "单参数 f(int)" << endl;
}
int main()
{
f();
f(10);
return 0;
}
③ 参数类型顺序不同
#include<iostream>
using namespace std;
// 参数个数一样、类型一样,但顺序不同,构成重载
void f(int a, char b)
{
cout << "f(int, char)" << endl;
}
void f(char b, int a)
{
cout << "f(char, int)" << endl;
}
int main()
{
f(10, 'a');
f('a', 10);
return 0;
}
❌ 易错点 1:仅返回值不同,不能构成重载
#include<iostream>
using namespace std;
// 报错!只有返回值不同,编译器无法区分
// void fxx(){}
// int fxx(){ return 0; }
int main()
{
// fxx(); 编译器不知道调用哪一个
return 0;
}
⚠️ 易错点 2:重载 + 缺省参数,容易产生歧义
#include<iostream>
using namespace std;
void f1()
{
cout << "f1()" << endl;
}
// 带缺省参数
void f1(int a = 10)
{
cout << "f1(int)" << endl;
}
int main()
{
// f1(); // 打开这一行直接编译报错,存在调用歧义!
f1(20); // 这一句没问题,明确匹配第二个
return 0;
}
十一、 引用
11.1 引用的概念和定义
引用并不是创建一个全新的变量,而是给已经存在的变量起一个别名。编译器不会为引用单独开辟内存空间,引用和原变量共用同一块内存。 可以通俗理解:孙悟空,外号齐天大圣,又叫美猴王;叫法不一样,但指代的是同一个角色。
语法格式:
类型& 引用别名 = 引用对象;注意:这里的&是引用标记,不是取地址符号,需要结合上下文区分。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
// b是a的别名
int& b = a;
// c也是a的别名,一个变量可以有多个别名
int& c = a;
// 给别名再起别名,d依旧是a的别名
int& d = b;
++d; // 修改d,本质就是修改a
// 打印地址,四个地址完全一样
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
11.2 引用的三大特性
引用在定义的时候必须初始化 同一个变量,可以拥有多个引用(多个别名) 引用一旦绑定到某个变量,就不能再绑定其他变量(只能赋值,不能改指向)
#include<iostream>
using namespace std;
int main()
{
int a = 10;
// int& ra; // 报错!引用没有初始化
int& b = a; // b绑定a
int c = 20;
b = c; // ⚠️这是赋值操作,不是让b改成绑定c
// b依旧是a的别名,只是把c的值拷贝给a
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
11.3 引用的使用场景
引用最常用的两个场景:引用传参、引用作为返回值。
示例 1:引用传参实现交换函数(Swap)
#include<iostream>
using namespace std;
// rx、ry是x、y的别名
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 1, y = 2;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
示例 2:引用在数据结构中的简化用法(栈初始化演示)
#include<iostream>
#include<cstdlib>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
// rs是外面栈对象的引用,不用传指针
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
int main()
{
ST st1;
STInit(st1);
return 0;
}
示例3:引用作为返回值
除了传参,引用也可以作为函数返回值,本质是返回目标变量的别名,省去值返回时的数据拷贝,提升运行效率。
这里有一条非常关键的约束:被返回的变量,它的生命周期一定要比函数更长。如果函数结束时这个变量已经被销毁,返回的引用就变成野引用,程序结果随机,甚至直接崩溃。 普通局部变量存放在栈空间,函数调用结束就会被回收,因此不能返回它的引用;而 static 静态变量、全局变量、外部传入的参数生命周期持久,可以安全地返回引用。
❌ 错误示例:返回局部变量的引用(野引用)
#include<iostream>
using namespace std;
int& Func()
{
int n = 10; // 局部变量,函数结束内存就回收
return n; // 危险,产生野引用,属于未定义行为
}
int main()
{
int& ret = Func();
cout << ret << endl;
return 0;
}
✅ 正确示例:返回 static 静态变量的引用
#include<iostream>
using namespace std;
int& Func()
{
static int n = 10; // static变量存放在静态区,直到程序结束才销毁
return n;
}
int main()
{
int& ret = Func();
cout << ret << endl;
ret = 20; // 通过引用直接修改原变量
cout << Func() << endl;
return 0;
}
引用返回常见的用途:一是减少大对象拷贝,优化性能;二是实现链式调用,支持连续操作。 记住禁忌:不要返回函数内部普通局部栈变量的引用!
💡补充:C++ 引用和 Java 引用不一样!C++ 引用绑定之后无法更换目标;引用和指针各有优势,不能完全互相替代。很多 C 语言写的数据结构教材,改用引用传参来避开复杂的二级指针。
11.4 const 引用
权限原则:引用过程中,访问权限可以缩小,但是不能放大
- 普通引用不能引用 const 常量(权限放大,报错)
- const 引用可以引用普通变量、const 常量
- 临时对象具有 const 属性,只能用 const 引用接收
#include<iostream>
using namespace std;
int main()
{
const int a = 10;
// int& ra = a; // 报错:把只读的a交给可修改的引用,权限放大
const int& ra = a; // ✅ 正确,const引用匹配const变量
int b = 20;
const int& rb = b; // ✅ 正确,权限缩小(本来可改,现在只读)
// 表达式计算产生临时对象,临时对象自带常性
const int& rc = 12;
const int& rd = b * 3;
return 0;
}
11.5 指针和引用的关系
很多同学学到这里容易把指针和引用搞混,C++ 里指针和引用就像性格不一样的一对兄弟,功能上有重叠、可以实现相似效果,但特点不同,不能互相替代。
- 概念空间:引用是变量的别名,不单独开辟内存;指针是独立变量,用来存放地址,需要开辟内存。
- 初始化:引用定义时必须初始化;指针语法上不强制初始化,但开发里建议初始化,避免野指针。
- 指向变更:引用一旦绑定某个变量,就不能再绑定别的对象;指针可以随时修改,指向不同变量。
- 访问方式:引用直接访问原变量;指针需要解引用
*才能访问指向的内容。 sizeof结果:sizeof(引用)得到的是原变量类型的大小;sizeof(指针)永远是地址的字节大小(32 位 4 字节,64 位 8 字节)。- 安全风险:指针很容易出现野指针、空指针问题;引用约束更多,使用起来相对安全
十二、 inline 内联函数
inline用来修饰函数,被修饰的函数叫做内联函数。编译阶段,编译器会尝试在调用内联函数的地方,直接把函数代码原地展开,省去函数调用建立栈帧的开销,以此提升效率。
⚠️ 重点:inline 只是给编译器的建议,不是强制命令。编译器觉得不合适的时候,可以选择不展开。递归函数、代码量大的函数,加 inline 基本都会被编译器忽略;inline 适合频繁调用、代码简短的小函数。
设计 inline 的核心目的:替代 C 语言的宏函数。宏是预处理阶段简单文本替换,写法繁琐、容易出错、不方便调试;内联函数兼具宏的高效,又拥有普通函数的类型检查,更加安全。
补充小细节:VS Debug 模式默认不会展开 inline 方便调试,如果想在 Debug 下观察展开效果,需要手动修改编译选项; inline 不建议声明和定义分离到不同文件,分离会出现链接报错,因为内联展开之后函数没有独立地址,链接找不到符号。
✅ inline 示例
#include<iostream>
using namespace std;
// 内联函数
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
// 可以看汇编判断是否展开:有call指令代表没展开
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}
🆚 对比 C 语言宏的坑
#include<iostream>
using namespace std;
// 宏很容易踩坑,括号少一个结果就出错
#define ADD(a, b) ((a) + (b))
int main()
{
int ret = ADD(1, 2);
cout << ADD(1, 2) << endl;
cout << ADD(1, 2)*5 << endl;
int x = 1, y = 2;
ADD(x & y, x | y);
return 0;
}
❌ 错误写法:inline 声明定义分离(链接报错)
// 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);
return 0;
}
报错原因:inline 展开后没有函数地址,外部文件找不到这个函数符号。
十三、nullptr
在 C 语言里我们习惯用NULL表示空指针,本质上NULL是一个宏。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
C++ 中NULL会被定义成字面常量 0,这里就会带来重载歧义的坑:当同时存在f(int)和f(int*)重载版本,传入NULL会优先匹配f(int),而不是我们想要的指针版本,和预期不符。
所以 C++11 引入了nullptr,它是专门代表空指针的关键字,是指针类型字面量,只能隐式转换成各种指针类型,不能转为整数,完美解决 NULL 带来的重载二义性问题。
示例: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),不是预期的指针版本
f(nullptr); // ✅ 匹配 f(int*),专门代表空指针
return 0;
}
好啦,那这篇内容就先整理到这里,这次把C++入门基础的相关知识整理了一下,为了方便自己日后复习回顾,也希望能给正在学习的小伙伴一些参考。如果文中有理解不到位或是书写有误的地方,欢迎大家在评论区指出,一起交流共同进步。谢谢大家!
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2602_95684935/article/details/168973818



