1. C语言和C++的区别
C 和 C++ 不是「两门互不相干的课」,而是「C 是 C++ 的子集式地基」。学完 C 再学 C++,重点不是重学语法,而是理解 C++ 在 C 之上加了什么。
| 维度 | C | C++ |
|---|---|---|
| 编程范式 | 面向过程 | 面向过程 + 面向对象 + 泛型 |
| 数据与操作 | 数据与函数分离 | 用类把数据和操作封装在一起 |
| 内存管理 | malloc/free,手动管理 |
new/delete,配合 RAII 与智能指针自动管理 |
| 复用方式 | 函数复用 | 函数重载、模板、继承 |
| 输入输出 | scanf/printf |
std::cin/std::cout,类型安全 |
| 类型检查 | 相对宽松 | 更严格,const、引用、强类型枚举等 |
| 标准库 | 少量标准库 | STL:容器、迭代器、算法 |
几个最直观的差异点:
- 引用 :
int& r = a;是a的别名,不是指针,函数传参优先用引用,避免拷贝; - 函数重载与默认参数:同名函数可以按参数类型/个数区分,C 不支持;
- 命名空间 :C++ 用
namespace解决全局命名冲突,C 只能靠前缀; const语义更强 :const引用、const成员函数是 C++ 的日常习惯;- 面向对象:类、封装、继承、多态是 C++ 与 C 差别最大的一块,也是面试高频区。
一句话总结:C 教会我「内存和指针」,C++ 在此基础上补上「抽象、泛型、资源管理」三层能力。
2. 命名空间
背景
在写代码过程中,变量、函数以及之后要学的类都会大量出现,而在多人协作中不可避免会出现命名冲突的情况。假设你和同事都写了 add() 函数,合并时编译器无法区分,命名空间就是为了解决这个问题而出现的。
定义
定义方式:namespace 命名空间的名字 {},{} 中是命名空间的成员,可以定义变量、函数、类型等。
namespace 的本质是定义出了一个域,这个域与全局域相互独立,不同的域可以定义相同的变量名。就像是从一个大房间里分出客厅、主卧、次卧一样,两个卧室都有床,但不会冲突。C++ 标准库将所有东西(比如 cout、string......)都放在了 std 这个命名空间中,所以在使用某些关键字时会加上 std::。
基本用法
有三种用法,各有优势劣势。
- 针对整个命名空间,比如
using namespace std;,方便省事但代价很大。如果自己的代码中有sort(),std中也有sort(),命名冲突就又出现了,不建议在大型项目或者头文件中这么干。using可以将命名空间中的某成员展开。
cpp
#include <iostream>
using namespace std; // 把 std 的门整个打开
int main() {
cout << "hello" << endl; // 不用写 std:: 了
cout << 123 << endl;
}
- 精确声明,
using std::引入的关键字,只会开放需要的东西,其他std里的名字不会冲突。
cpp
#include <iostream>
using std::cout; // 只把 cout 引进来
using std::endl; // 只把 endl 引进来
int main() {
cout << "hello" << endl; // 方便且安全
}
- 保持每段代码都写,比如
std::cout,虽然繁琐,但最安全,也能方便读者看出哪个是标准库的,哪个是自己的。
cpp
#include <iostream>
int main()
{
std::cout << "hello" << std::endl;
}
注意
namespace只能定义在全局域,且不影响生命周期(比如定义在函数局部域中的变量会在函数执行完后销毁),可以嵌套定义。- 项目工程中不同文件中的同名
namespace会被认为是同一个namespace。namespace不是「创建一个东西」,而是「声明一片领地」:你在文件 A 声明了school领地,文件 B 再声明同名的school领地,编译器说「哦,是同一块地,接着用就行」。 - 精确声明很常用,不仅保留了命名空间的价值,而且只消除了重复输入的冗余,在大型项目中可读性更高。
3. 输入与输出
定义
<iostream> 的全称是 input output stream,即 C++ 的输入输出流,流的本质是一个数据通道。
cin是istream类的对象 → 专门负责「从键盘读入数据」;cout是ostream类的对象 → 专门负责「向屏幕写出数据」;<<与>>被重载了:C 语言中作为左移/右移,C++ 中作为流插入/流提取,编译器会根据该符号左右两边的类型,决定它是位运算还是流操作;endl做了两件事:插入换行符\n,并强制刷新缓冲区------把内存中的数据立刻推到屏幕上。
基本用法
(1)基本输出:自动识别类型
cpp
#include <iostream>
#include <string>
int main() {
int a = 10;
double b = 3.14159;
char c = 'X';
std::string msg = "Hello";
std::cout << "int: " << a << std::endl;
std::cout << "double: " << b << std::endl;
std::cout << "char: " << c << std::endl;
std::cout << "string: " << msg << std::endl;
return 0;
}
(2)链式调用:连续 <<
<< 的返回值是流对象本身,所以可以一路串下去:
cpp
std::cout << a << " + " << b << " = " << (a + b) << std::endl;
(3)endl 与 \n 的区别
cpp
std::cout << "这是用 \\n 换行\n";
std::cout << "这是用 endl 换行" << std::endl;
// 两者看起来一样,但 endl 会额外刷新缓冲区
(4)cin 输入
cpp
std::string input_name;
int input_age;
std::cout << "请输入你的名字和年龄(用空格隔开): ";
std::cin >> input_name >> input_age;
std::cout << "收到!你叫 " << input_name << ",今年 " << input_age << " 岁。" << std::endl;
(5)自定义类型输出:重载 <<
cpp
#include <iostream>
#include <string>
struct Student {
std::string name;
int age;
double gpa;
};
// 重载 << 运算符,让 Student 可以直接被 cout 输出
std::ostream& operator<<(std::ostream& os, const Student& s) {
os << "姓名: " << s.name << " | 年龄: " << s.age << " | GPA: " << s.gpa;
return os;
}
int main() {
Student s1 = {"张三", 20, 3.85};
Student s2 = {"李四", 22, 4.0};
std::cout << "学生1 -> " << s1 << std::endl;
std::cout << "学生2 -> " << s2 << std::endl;
return 0;
}
4. 缺省参数
定义
缺省参数相当于给函数的参数提前设一个「备用值」,调用该函数时如果没传参数,就会使用备用值,否则就会使用传递过去的参数。
cpp
// 给参数 b 一个缺省值 10
void func(int a, int b = 10) {
cout << "a = " << a << ", b = " << b << endl;
}
func(3); // 输出: a = 3, b = 10(b 用了缺省值)
func(3, 20); // 输出: a = 3, b = 20(b 用了你传的值)
注意
- 缺省参数分为全缺省和半缺省,半缺省必须从右往左连续缺省,不能跳跃,否则会产生歧义。
cpp
// ✅ 正确:从右往左连续缺省
void func1(int a, int b = 2, int c = 3);
// ✅ 也正确:只给最右边的 c 缺省值,同样是「从右往左连续」
void func2(int a, int b, int c = 3);
// ❌ 错误:中间 b 没有缺省值,缺省不连续
void func3(int a = 1, int b, int c = 3);
- 调用时必须从左到右依次给实参,不能跳跃。
cpp
void func(int a, int b = 2, int c = 3);
func(1, , 5); // ❌ 想跳过 b 只给 c?不行!语法不允许
- 声明和定义分离时,缺省值只能写在声明处。
cpp
// ✅ 正确:写在声明里
void func(int a = 10);
void func(int a) {
cout << a << endl;
}
// ❌ 错误:声明和定义都写缺省值
void func(int a = 10);
void func(int a = 10) { ... } // 编译报错
5. 重载函数
定义
本质是一个函数名、多种用法。C 语言中要求函数名必须不同,但为了更加灵活方便,C++ 允许函数名相同的情况,前提是形参的个数或类型不同。
就像去奶茶店直接说「来一杯奶茶」(调用同名函数):如果什么都不说,店员会给一杯原味的;如果要加珍珠,店员就给一杯带珍珠的;如果要加椰果,店员就给一杯带椰果的。虽然都是一杯奶茶(函数名相同),但根据不同的要求或配料(参数的个数与类型不同),执行后的结果是不同的。
用法
举个例子:
cpp
#include <iostream>
using namespace std;
// 版本1:两个整数相加
int add(int a, int b) {
return a + b;
}
// 版本2:三个整数相加(参数个数不同)
int add(int a, int b, int c) {
return a + b + c;
}
// 版本3:两个小数相加(参数类型不同)
float add(float a, float b) {
return a + b;
}
int main() {
// 编译器会自动根据你传的「配料」,去挑对应的函数
cout << add(1, 2) << endl; // 传了两个整数,调用版本1,输出 3
cout << add(1, 2, 3) << endl; // 传了三个整数,调用版本2,输出 6
cout << add(1.5f, 2.5f) << endl; // 传了两个小数,调用版本3,输出 4
return 0;
}
注意
- 只能靠参数的类型或个数不同来区分同名函数,不能只靠返回值来区分。
cpp
int add(int a, int b);
double add(int a, int b);
// 这种情况是无法区分的
- 底层原理:编译器在编译阶段会对函数名做名字修饰(name mangling) ,把参数类型信息编码进符号名里。比如
add(int, int)和add(float, float)修饰后是两个不同的符号,链接器才能区分它们。返回值不参与修饰,所以只改返回值无法构成重载。
6.引用:
定义:
引用是某个已存在对象的"别名",不是新对象;对引用的操作就是对被引用对象的操作。
类型& 变量名 = 引用对象
常见用法:
- 函数参数:修改实参,避免拷贝,相比于C语言,引用版本更安全,不能传空,调用时不用取地址,函数内部不用解引用
cpp
void swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
int a = 1, b = 2;
swap(a, b); // 不需要传地址,也不需要解引用
//C语言指针:
void swap(int* x, int* y);
swap(&a, &b);
在C语言中,传值就相当于拷贝一份给函数使用,函数内部修改形参,不会影响到实参的值,但拷贝的代价时小时大,如果只拷贝一个int则无所谓,如果参数是std::string呢?
cpp
void printVector(std::vector<int> v) { // 按值:整个 vector 被复制
for (int x : v) std::cout << x << ' ';
}
std::vector<int> big(1000000); // 100 万个 int,约 4 MB
printVector(big); // 复制 4 MB 数据,再释放
//用const引用
void printVector(const std::vector<int>& v) { // 不拷贝
for (int x : v) std::cout << x << ' ';}
这就好比,想让朋友看一下自己的书(传参),但不可以把整本书都复印一遍(传值)过去看完再烧了(释放空间),如果只是借给他,他只能看不能勾画(const 类型名& 变量名,表示只读)或者可以勾画(类型名& 变量名)的做法,效率就会高很多
适用情况有以下:
1.1 大对象不复制
cpp
struct Big {
int data[10000];
};
void f(Big b); // 拷贝 40000 字节
void f(const Big& b); // 只传地址,通常 8 字节
1.2 有些类型不允许拷贝,或拷贝有副作用,不是所有东西都能随便复制,比如std::unique_ptr 表示独占所有权,拷贝它没有意义(两个指针都以为自己独占同一块内存)。类似地,文件流、锁、数据库连接等资源型对象通常禁止拷贝,只能传引用。
1.3 我不想改原对象,也不想付拷贝代价
cpp
void log(const std::string& msg); // 只读,不拷贝
其实在C语言中,当结构体作为变量,给函数传参时我们一般传递的是指向结构体的指针而不是整个结构体,也是为了避免拷贝,为了预防空指针,我们一般还加个if判断或者assert
- const引用,只读且可绑定临时变量,当局部const引用绑定临时变量时,临时对象的生命周期会延长到引用变量的生命周期结束,但函数返回const引用绑定临时量不会延长生命周期,而是悬垂(未定义行为,会报错)
cpp
void print(const std::string& s);
print("hello"); // ✅ const 引用可以绑定临时对象
//但普通引用不能绑定临时变量
std::string& s = std::string("hi"); // ❌
const std::string& s = std::string("hi"); // ✅
const int& r = 42; // 合法,临时量生命周期延长
const std::string& bad() {
return std::string("temp"); // ❌ 返回后悬垂
}
- 函数返回值引用,普通函数返回值是拷贝一份给调用者,而返回引用是把对象本身(的别名)交出去
cpp
int& getRef() {
static int x = 10;
return x; // 返回 x 本身的引用
}
int& r = getRef();
r = 20; // 直接修改了 getRef 里的 x
所以这里就需要注意,交出去的对象还存在吗,万一被释放了,那后续再用就是未定义行为了
可以安全返回的情况
3.1 返回全局变量/静态变量
3.2 返回类成员
3.3返回传入的引用参数
3.4返回堆上对象
危险的情况
3.1返回局部变量引用
3.2返回临时量的引用
3.3返回局部const引用的"延长"
下面的部分,包括范围for循环,多态引用,类成员引用之类的会在之后写到,这里仅作了解
- 范围for循环
语法是for (声明 : 容器) { 循环体 }
cpp
std::vector<std::string> names{"张三", "李四", "王五"};
// 只读打印:const auto& 最好,0 拷贝
for (const auto& name : names)
std::cout << name << '\n';
// 每个名字加感叹号:auto& 直接改原元素
for (auto& name : names)
name += "!";
// 只是想要副本做别的事:auto
for (auto name : names) {
// 这里改 name 不影响 names
}
- 多态引用
- 类成员引用
注意:
- 引用必须初始化,如果变量以及其背后所占用的内存是一间房子,那变量的别名就是这个房间的另一张门牌号
- 引用一旦绑定,不能重新绑定,一张门牌号只能用于一间房子
- 没有"空引用",引用必须绑定到一个有效对象。但要注意:没有空引用不等于永远安全,可能出现"悬垂引用"------被引用对象已经销毁,引用还在。房子拆了,如果还用原来的门牌号去找房子,可能找不到,也可能找到其他的新建的房子
- 语言层面引用不是对象,不能定义"引用的引用"的普通变量(模板引用折叠除外),不能有引用数组,不能定义指向引用的指针,但可以取引用的地址:
cpp
int a = 1;
int& r = a;
int& arr[3]; // ❌ 不能有引用数组
int&& rr = r; // 这不是"引用的引用"的普通用法
int* p = &r; // ✅ 可以取地址,得到的是 a 的地址
sizeof(引用)得到的是被引用对象的大小,不是引用本身的大小。
cpp
sizeof(r) == sizeof(a) // 得到的是被引用对象的大小
- 引用通常底层用指针实现,但语义不同:引用更安全、更自然,不需要解引用,不能为空,不能改绑。
7. 内联函数
背景
在 C 语言中调用函数时,CPU 会把返回地址记下来,把现在的参数压入栈,跳到函数代码的地方,执行完再跳回来并清理栈,我们管它叫建立栈帧。有时候这个代价太大了,就像为了喝一杯水需要专门跑到水房喝完再回来一样。为了提高效率,产生了内联函数。
概念
通过直接在调用处把代码展开,省去建立栈帧的步骤,提高效率。内联函数通过 inline 修饰。
注意
-
内联函数只适合代码短小(比如 1~5 行)且频繁调用(比如在循环里)的情况。如果写了个递归让它自己展开自己,会导致死循环;或者写了个调用几百次、几百行的代码,展开会导致 CPU 内代码体积膨胀,缓存变小变慢。
-
inline可以替代 C 语言的宏。C 语言想要实现直接贴代码而不建立栈帧需要依靠宏(就是#define那行),会在预处理阶段直接把文字替换进去,有很多缺点,比如容易写错(你少写一个括号#define ADD(a, b) a + b,那么ADD(1, 2) * 3就会变成1 + 2 * 3,结果是 7 而不是 9),也无法调试(你在代码里打个断点,调试器根本停不下来,因为代码已经被替换得面目全非了)。
cpp
#define ADD(a, b) ((a) + (b))
-
VS Debug 版本下默认不展开,方便调试。如果你写了一个
inline函数,里面有个 Bug,如果 Debug 模式下把它展开了,你单步调试(F10/F11)的时候,代码会「唰」地一下跳过函数,或者进去之后找不到函数名,你根本不知道自己走到哪了。所以 VS 在 Debug 默认不展开,让你正常进入函数去查 Bug;等到了 Release(发布版),它再悄悄帮你展开提速。 -
内联函数的声明和定义不能分离。普通函数可以做到:
add.h里写:int Add(int x, int y);(声明)add.cpp里写:int Add(int x, int y) { return x + y; }(定义)main.cpp里#include "add.h",然后调用Add。
普通函数在编译后,会拥有一个内存地址,main 调用它时会把 add 指向相应的地址;但内联函数会在编译阶段展开到调用的地方,并不会生成内存地址,也就导致链接时找不到。解决方法是把声明和定义一起写到头文件里:
cpp
// add.h
inline int Add(int x, int y) {
return x + y;
}
- 补充一点:
inline只是建议编译器展开,而不是强制命令。编译器有权拒绝(比如函数体太大、含递归、含循环等),最终是否展开由编译器决定。另外,类内定义的成员函数默认就是内联的。
8.总结
这篇笔记从「C 到 C++ 的过渡」出发,把入门阶段最核心的几个知识点串了一遍:
- 区别与定位:C 是地基,C++ 在它之上补上了抽象、泛型与资源管理,学 C++ 的重点是理解「新增了什么」,而不是重学语法;
- 命名空间 :用
namespace划分独立的域来解决命名冲突,using namespace方便但危险,精确声明using std::cout是更稳妥的日常选择; - 输入输出 :
cin/cout是流对象,<<、>>被重载为流操作,endl除了换行还会刷新缓冲区; - 缺省参数:给参数准备「备用值」,半缺省必须从右往左连续,且缺省值只能写在声明处;
- 重载函数:同名函数靠参数个数或类型区分,不能只靠返回值区分;
- 引用 :对象的别名,必须初始化、不能改绑、没有空引用,常用于函数传参(避免拷贝)、
const引用绑定临时量、返回引用时注意悬垂问题; - 内联函数 :用
inline在调用处展开代码以省去建立栈帧的开销,适合短小且高频调用的函数,声明与定义不能分离。
整体思路可以概括为:先建立「C++ 在 C 之上加了什么」的全局认知,再逐个吃透命名空间、输入输出、缺省参数、重载、引用、内联这些高频基础点。这些内容既是后续学习类与对象、STL 的前置知识,也是笔试面试中反复出现的考点,值得多写代码、多踩坑来巩固。