C++ 并不是“加了面向对象的 C”,而是在兼容 C 的基础上,为大型工程开发补充了更强的抽象能力、类型安全和代码组织能力。
对初学者来说,先掌握命名空间、输入输出、缺省参数、函数重载、引用、内联函数和nullptr,才能真正迈出从 C 到 C++ 的第一步。
一、为什么要学习 C++?
C++ 诞生于对 C 语言的扩展需求。
C 语言执行效率高、接近底层、适合系统开发,但随着项目规模扩大,也会逐渐暴露出一些问题:
全局变量和函数容易重名;
函数不支持重载;
参数传递常常依赖指针;
自定义类型的输入输出不够方便;
大型工程的组织与抽象能力有限。
C++ 在保留 C 高性能特点的同时,逐步加入了:
命名空间 引用 函数重载 缺省参数 类与对象 模板 STL 异常 智能指针 并发库因此,C++ 广泛应用于:
操作系统、编译器、数据库;
游戏引擎与图形渲染;
音视频处理;
高性能服务端;
嵌入式开发;
浏览器、客户端软件;
金融交易与量化系统;
人工智能基础设施。
二、第一个 C++ 程序
#include <iostream> int main() { std::cout << "Hello C++" << std::endl; return 0; }程序中几个关键部分:
| 代码 | 含义 |
|---|---|
#include <iostream> | 包含 C++ 标准输入输出流库 |
std::cout | 标准输出对象 |
<< | 流插入运算符 |
std::endl | 换行并刷新输出缓冲区 |
return 0 | 表示程序正常结束 |
输出结果:
Hello C++在日常代码中,也可以使用换行符:
std::cout << "Hello C++\n";一般情况下,\n更轻量;std::endl除了换行,还会强制刷新缓冲区,因此不建议在大量输出时滥用。
三、命名空间:解决“名字冲突”问题
1. 什么是命名冲突?
在 C 或 C++ 中,变量、函数、类型都需要名字。
但随着项目变大,多个模块、第三方库、系统库中很容易出现同名标识符。
例如:
#include <cstdlib> int rand = 10; int main() { return 0; }这里的rand与标准库中的rand()函数发生冲突,程序无法正常编译。
这就是命名污染问题。
2. namespace 的作用
C++ 引入了命名空间namespace,用于把相关变量、函数和类型放到一个独立作用域中。
namespace my_project { int version = 1; int Add(int left, int right) { return left + right; } }使用命名空间中的成员时,需要加作用域限定符:::
#include <iostream> int main() { std::cout << my_project::version << '\n'; std::cout << my_project::Add(10, 20) << '\n'; return 0; }输出:
1 30可以把my_project::Add理解为:
my_project 这个区域中的 Add 函数3. 命名空间可以嵌套
namespace company { namespace project { int value = 100; } }访问方式:
std::cout << company::project::value << '\n';现代 C++ 还支持更简洁的写法:
namespace company::project { int value = 100; }4. 命名空间的三种使用方式
假设有如下命名空间:
namespace demo { int a = 10; int b = 20; }方式一:指定命名空间访问,最推荐
std::cout << demo::a << '\n'; std::cout << demo::b << '\n';优点:
来源明确;
不易冲突;
适合工程代码。
方式二:只引入某个成员
using demo::a; int main() { std::cout << a << '\n'; std::cout << demo::b << '\n'; return 0; }这种方式适合少量、确定不会冲突的名称。
方式三:展开整个命名空间
using namespace demo; int main() { std::cout << a << '\n'; std::cout << b << '\n'; return 0; }这种写法在小练习中很方便。
但在实际项目中,通常不建议在头文件或大型源文件中写:
using namespace std;因为std中包含大量名字,可能与项目中的变量、函数或第三方库发生冲突。
更推荐:
std::cout << "Hello\n"; std::vector<int> nums;四、C++ 输入与输出:cin、cout 与 endl
C 语言常用:
printf("%d\n", num); scanf("%d", &num);C++ 提供流式输入输出:
#include <iostream> int main() { int age = 0; double score = 0.0; char grade = '\0'; std::cin >> age >> score >> grade; std::cout << "年龄:" << age << '\n'; std::cout << "分数:" << score << '\n'; std::cout << "等级:" << grade << '\n'; return 0; }特点是无需手动指定%d、%lf等格式控制符。
std::cin >> age; std::cout << age << '\n';编译器会根据变量类型完成对应处理。
1.<<和>>的含义
std::cout << value;表示把value插入输出流。
std::cin >> value;表示从输入流中提取数据并存入value。
可以连续使用:
int a = 0; int b = 0; std::cin >> a >> b; std::cout << a << ' ' << b << '\n';2. 大量输入输出时的性能优化
在竞赛或高频 I/O 场景中,常写:
std::ios::sync_with_stdio(false); std::cin.tie(nullptr);完整示例:
#include <iostream> int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); int n = 0; std::cin >> n; std::cout << n << '\n'; return 0; }这两行通常要放在程序最开始、第一次 I/O 之前。
注意:关闭同步后,不建议再混用scanf/printf与cin/cout,否则输出顺序和行为可能不符合预期。
五、缺省参数:让函数调用更灵活
1. 什么是缺省参数?
缺省参数也叫默认参数。
定义函数时,可以提前给形参设置默认值;调用函数时,如果没有传入对应实参,就会使用默认值。
#include <iostream> void PrintInfo(const char* name = "游客") { std::cout << "你好," << name << '\n'; } int main() { PrintInfo(); PrintInfo("张三"); return 0; }输出:
你好,游客 你好,张三2. 全缺省参数与半缺省参数
全缺省参数
所有参数都有默认值:
void Func(int a = 10, int b = 20, int c = 30) { std::cout << a << ' ' << b << ' ' << c << '\n'; }可以这样调用:
Func(); Func(1); Func(1, 2); Func(1, 2, 3);半缺省参数
只给部分参数设置默认值:
void Func(int a, int b = 10, int c = 20) { std::cout << a << ' ' << b << ' ' << c << '\n'; }调用方式:
Func(100); Func(100, 200); Func(100, 200, 300);3. 半缺省参数的规则
缺省参数必须从右向左连续给出。
正确写法:
void Func(int a, int b = 10, int c = 20);错误写法:
// 错误:不能跳过 b,直接给 c 默认值 void Func(int a, int b = 10, int c);调用时也只能从左向右传参,不能跳过中间参数。
4. 声明与定义分离时的注意点
头文件中声明:
// Func.h void Func(int a, int b = 10);源文件中定义:
// Func.cpp void Func(int a, int b) { std::cout << a + b << '\n'; }默认参数通常只放在函数声明处。
不要在声明和定义中重复写默认值,否则会产生重复默认实参错误。
六、函数重载:同名函数也可以共存
1. 什么是函数重载?
C++ 允许在同一作用域中定义同名函数,只要它们的参数列表不同。
#include <iostream> int Add(int left, int right) { return left + right; } double Add(double left, double right) { return left + right; } int main() { std::cout << Add(10, 20) << '\n'; std::cout << Add(1.5, 2.5) << '\n'; return 0; }编译器会根据实参类型自动选择合适的函数。
输出:
30 42. 哪些情况构成函数重载?
参数类型不同
void Print(int x); void Print(double x);参数个数不同
void Print(); void Print(int x);参数顺序不同
void Print(int x, char ch); void Print(char ch, int x);3. 返回值不同不能构成重载
下面的写法错误:
int Func(); double Func();因为调用时:
Func();编译器无法仅依靠返回值判断应该调用哪个版本。
所以函数重载只看:
函数名 + 参数列表不看返回值。
4. 缺省参数与函数重载可能产生歧义
void Func() { } void Func(int x = 10) { }此时调用:
Func();会产生二义性。
因为两个函数都能匹配这次调用,编译器不知道该选哪一个。
因此,设计重载函数时要避免让默认参数制造多个可行候选函数。
七、引用:给变量取别名
1. 引用的基本概念
引用不是创建新变量,而是给已经存在的变量取一个别名。
int a = 10; int& ra = a;这里:
a 是原变量 ra 是 a 的别名修改ra就是在修改a。
#include <iostream> int main() { int a = 10; int& ra = a; ra = 100; std::cout << a << '\n'; return 0; }输出:
1002. 引用的三个重要特性
引用定义时必须初始化
int a = 10; int& ra = a;下面写法错误:
// int& ra;引用必须在创建时绑定到某个对象。
一个对象可以有多个引用
int a = 10; int& r1 = a; int& r2 = a; int& r3 = a;r1、r2、r3都是a的别名。
引用不能“改绑”
int a = 10; int b = 20; int& r = a; r = b;很多初学者会误以为最后一行让r改为引用b。
实际上不是。
r = b;本质是:
a = b;引用一旦绑定到a,就不能改为绑定其他对象。
八、引用传参:比指针传参更直观
C 语言中,交换两个变量通常使用指针:
void Swap(int* px, int* py) { int tmp = *px; *px = *py; *py = tmp; }调用时:
Swap(&a, &b);C++ 可以使用引用:
#include <iostream> void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { int a = 10; int b = 20; Swap(a, b); std::cout << a << ' ' << b << '\n'; return 0; }输出:
20 10引用传参的优点:
不需要写
&a;函数内部不需要写
*px;语义更接近“直接操作实参”;
可以避免传递空指针。
但引用并不能完全替代指针。
当“对象可能不存在”需要用空值表示时,指针依然很重要。
例如:
int* Find(int* arr, int n, int target);返回nullptr可以明确表示“没有找到”。
九、const 引用:高效且安全的传参方式
1. 为什么要使用 const 引用?
如果函数参数是一个大对象,按值传参会发生拷贝。
例如:
void Print(std::string str);调用时会复制一份字符串。
更好的写法:
void Print(const std::string& str);这样既不会产生不必要的拷贝,又能保证函数内部不修改原对象。
#include <iostream> #include <string> void Print(const std::string& str) { std::cout << str << '\n'; } int main() { std::string message = "Hello C++"; Print(message); return 0; }2. const 引用可以绑定临时对象
const int& r1 = 10; const int& r2 = 1 + 2;10和1 + 2的结果都是临时对象,普通引用不能绑定它们。
错误写法:
// int& r = 10;正确写法:
const int& r = 10;3. const 引用可以“缩小权限”
int a = 10; const int& ra = a;虽然a本身可以修改,但通过ra不能修改。
// ra = 20; // 错误 a = 20; // 正确这体现了一个重要原则:
权限可以缩小,不能放大。
十、引用返回:必须关注对象生命周期
引用可以作为函数返回值:
int& Front(int* arr) { return arr[0]; }这样调用者可以直接修改原数组中的第一个元素:
int arr[] = {1, 2, 3}; Front(arr) = 100;此时arr[0]变成100。
但是绝对不能返回局部变量的引用:
int& Func() { int x = 10; return x; // 错误:x 在函数结束后已经销毁 }局部变量离开函数作用域后生命周期结束,返回它的引用会形成悬空引用,属于未定义行为。
判断能否返回引用的核心是:
被返回对象在函数结束后是否仍然存在?
十一、指针与引用的区别
| 对比项 | 指针 | 引用 |
|---|---|---|
| 本质 | 保存对象地址的变量 | 已有对象的别名 |
| 是否必须初始化 | 语法上不是必须,但强烈建议 | 必须初始化 |
| 是否可改变指向 | 可以 | 不可以 |
| 是否可能为空 | 可以为nullptr | 正常使用中不应为空 |
| 访问对象方式 | 需要解引用*ptr | 可直接使用 |
sizeof结果 | 指针自身大小 | 被引用类型大小 |
| 典型用途 | 可选对象、动态内存、链表 | 参数传递、返回值、别名 |
实践中的建议:
参数必须存在且函数需要修改实参:优先考虑引用;
参数可能不存在:使用指针;
需要动态管理对象或改变指向:使用指针;
只读传递大型对象:优先使用
const T&。
十二、inline:不只是“建议编译器展开”
1. 什么是内联函数?
inline int Add(int x, int y) { return x + y; }传统理解是:编译器可能在调用位置直接展开函数代码,以减少函数调用开销。
int result = Add(1, 2);可能被优化成:
int result = 1 + 2;但要注意:
inline只是建议,不保证编译器一定展开。
现代编译器会根据优化级别、函数复杂度、调用频率等因素自行决定是否内联。
2. inline 更重要的意义
在现代 C++ 中,inline不仅和性能有关,还与多文件编译时的“多重定义规则”有关。
内联函数的定义通常应放在头文件中:
// Add.h #pragma once inline int Add(int x, int y) { return x + y; }多个源文件包含这个头文件是允许的。
不建议只在.cpp文件中定义inline函数,却在其他源文件中调用它。因为调用方可能看不到函数定义,导致链接问题。
3. inline 与宏函数
C 语言中常使用宏模拟小函数:
#define ADD(a, b) ((a) + (b))如果括号不够严谨,宏很容易出错。
例如:
#define ADD(a, b) a + b执行:
ADD(1, 2) * 5可能被替换成:
1 + 2 * 5结果为:
11而不是期望的:
15相比宏函数,内联函数有明显优势:
有类型检查;
遵循正常作用域规则;
更容易调试;
不会发生危险的文本替换;
参数只会被求值一次。
因此,C++ 中优先使用inline函数、constexpr函数或模板,而不是宏函数。
十三、nullptr:C++ 中更安全的空指针
1. NULL 的潜在问题
在 C++ 中,NULL很多时候被定义为整数常量0。
看下面的重载:
#include <iostream> void Func(int x) { std::cout << "Func(int)\n"; } void Func(int* p) { std::cout << "Func(int*)\n"; }调用:
Func(0); Func(NULL);可能调用的是:
Func(int)因为NULL可能被解释为整数0。
这与“我想传一个空指针”的意图不一致。
2. 使用 nullptr
C++11 引入了nullptr。
Func(nullptr);此时会明确调用:
Func(int*)因为nullptr是专门用于表示空指针的关键字。
推荐规则:
C++ 中表示空指针,优先使用 nullptr。 不要再使用 NULL。示例:
int* p1 = nullptr; // 不推荐 int* p2 = NULL;十四、一个综合示例
下面的程序综合使用了命名空间、默认参数、函数重载、引用和nullptr。
#include <iostream> namespace tools { void Print(int value) { std::cout << "整数:" << value << '\n'; } void Print(double value) { std::cout << "小数:" << value << '\n'; } void AddScore(int& score, int delta = 10) { score += delta; } void CheckPointer(const int* p) { if (p == nullptr) { std::cout << "指针为空\n"; } else { std::cout << "指针指向的值为:" << *p << '\n'; } } } int main() { int score = 80; tools::AddScore(score); tools::Print(score); tools::AddScore(score, 5); tools::Print(score); int number = 100; tools::CheckPointer(&number); tools::CheckPointer(nullptr); return 0; }输出:
整数:90 整数:95 指针指向的值为:100 指针为空十五、总结
从 C 进入 C++,最先需要建立的是几个新的思维方式:
用命名空间组织代码,避免名字冲突;
用
cin/cout进行类型安全的流式输入输出;用缺省参数减少重复调用;
用函数重载提升接口表达能力;
用引用简化传参,并减少不必要拷贝;
用
const T&高效、安全地传递大型只读对象;用
inline替代多数宏函数;用
nullptr替代NULL。
可以把这些特性理解为一句话:
C++ 在尽量不牺牲性能的前提下,让代码更安全、更易读,也更适合大型工程协作。