🐣 纯新手先看这一框(30 秒秒懂)
C++ 和 C 啥关系?C++ 是在 C 的基础上长出来的:C 的几乎所有写法在 C++ 里都能用,C++ 又加了“类/对象、模板、STL 标准库”等现代能力。
为什么要学它?写小程序 C 够用;写更大的项目(游戏、桌面软件、服务器),C++ 的“面向对象”和现成容器(vector/map)能省很多事。
怎么跑起来?同样靠“编译器”把代码翻成机器指令。本站「练习场」选 C++ 点运行即可,不用自己装环境;想深入再装 g++。
课程说明C 基础之上,系统掌握现代 C++
C++ 由 Bjarne Stroustrup 在 1979 年起基于 C 发展而来,目标是“带类的 C”,后来逐步加入模板、异常、STL、RTTI、智能指针、lambda 等。它几乎完全兼容 C,同时提供面向对象、泛型、函数式等抽象能力,既能写底层(操作系统、游戏引擎、嵌入式),也能写上层(桌面、服务、AI 框架)。
本课程假设你已学过 C(或看过 C 语言课程)。重点放在C++ 区别于 C 的现代特性:引用、类、模板、STL、RAII、智能指针——并反复强调“别把 C 的写法平移过来踩雷”。
g++ 或 clang++ 编译。强烈建议 -std=c++17 -Wall -Wextra。C++ 标准演进:C++98 → C++11(现代 C++ 起点)→ C++14 → C++17 → C++20。本课程以 C++17 常用子集为主,适当提及 C++11。
1. 从 C 到 C++iostream、命名空间、编译四阶段
#include <iostream>
using namespace std; // 引入标准命名空间
int main() {
cout << "Hello, C++!" << endl; // 输出到标准输出
int x;
cin >> x; // 从标准输入读
cout << "x = " << x << "\n";
return 0;
}
g++ -std=c++17 -Wall -Wextra hello.cpp -o hello
./hello
1.1 与 C 的核心区别
- IO:
cin/cout替代scanf/printf(也可混用<cstdio>)。 - 命名空间:标准库都在
std里,用std::cout或using namespace std;引入。 - 类型安全:
nullptr替代NULL;malloc不调用构造。 - 更宽松的声明位置:变量可在使用时定义(RAII 友好)。
using namespace std; 会把它“泄漏”到所有包含该头文件的文件,极易命名冲突。源文件里可写,但更推荐用 std:: 前缀,或只引入用到的名字(using std::cout;)。
2. 引用与 const别名、引用传参、const 正确性
引用是“变量的别名”,必须初始化且一旦绑定不能改指别处。比指针更安全、语法更干净,常用于函数参数与返回值。
#include <iostream>
using namespace std;
void swap(int& a, int& b) { // 引用传参,直接改外部
int t = a; a = b; b = t;
}
int main() {
int x = 1, y = 2;
swap(x, y);
cout << x << " " << y << "\n"; // 2 1
return 0;
}
2.1 三种常见引用用法
| 形式 | 含义 | 用途 |
|---|---|---|
T& | 普通左值引用 | 修改外部对象、避免大对象拷贝 |
const T& | 只读引用 | 只读访问且免拷贝(最常用参数类型) |
T&& | 右值引用 | 移动语义(第 16 节) |
const 的地方都加:const int& v 只读不改;int get() const 承诺成员函数不修改对象状态。const 正确性能让编译器帮你挡掉大量误改 bug,也是良好接口的标配。
3. 函数增强重载、默认参数、inline、函数模板雏形
#include <iostream>
using namespace std;
int add(int a, int b = 0) { // 默认参数(从右往左给)
return a + b;
}
int add(double a, double b) { // 函数重载:同名不同参数
return (int)(a + b);
}
inline int square(int x) { return x * x; } // 建议内联,减少小函数调用开销
int main() {
cout << add(3) << "\n"; // 3 (b 用默认值 0)
cout << add(3, 4) << "\n"; // 7
return 0;
}
f(int a, int b=0) 合法,f(int a=0, int b) 非法);重载靠参数类型/个数/const 区分,不能仅靠返回值区分。
4. string 字符串类自动管理、拼接、查找、与 C 字符串互转
std::string 自动管理内存、可动态增长,告别 char[] 与手动 \0 的麻烦。
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "hello";
s += " world"; // 拼接
s.append("!");
cout << s << " len=" << s.size() << "\n"; // hello world! len=12
cout << s.substr(0, 5) << "\n"; // hello
if (s.find("world") != string::npos) cout << "found\n";
return 0;
}
4.1 常用成员
| 成员 | 作用 |
|---|---|
size()/length() | 字符个数 |
empty() | 是否为空 |
s += t / s.append(t) | 拼接 |
s.substr(pos, n) | 取子串 |
s.find(t) | 查找,返回位置或 npos |
s.c_str() | 转 C 风格 const char* |
s.c_str() 返回的指针在 s 被修改或销毁后失效;若要把 C 字符串长期保存,应 strcpy 到自己的缓冲区或用 std::string 持有。
5. 类与对象class、访问控制、成员函数、this
#include <iostream>
#include <string>
using namespace std;
class Student {
private:
int id;
string name;
public:
void set(int i, string n) { id = i; name = n; }
void show() const { cout << id << " " << name << "\n"; }
int getId() const { return id; } // const 成员函数
};
int main() {
Student s;
s.set(1, "XiaoMing");
s.show();
return 0;
}
private 成员对外隐藏实现细节,只通过 public 接口交互——这是面向对象“封装”的核心。类的成员函数内有个隐式 this 指针指向调用对象。
6. 构造 / 析构 / 拷贝初始化列表、三/五法则、深浅拷贝
#include <iostream>
#include <cstring>
using namespace std;
class Buffer {
char* data;
int len;
public:
Buffer(int n) : len(n) { // 初始化列表
data = new char[n];
}
~Buffer() { delete[] data; } // 析构:释放资源
// 拷贝构造(深拷贝)
Buffer(const Buffer& o) : len(o.len) {
data = new char[len];
memcpy(data, o.data, len);
}
// 拷贝赋值
Buffer& operator=(const Buffer& o) {
if (this != &o) { // 防自赋值
delete[] data;
len = o.len;
data = new char[len];
memcpy(data, o.data, len);
}
return *this;
}
};
int main() {
Buffer a(10);
Buffer b = a; // 调用拷贝构造(深拷贝,互不影响)
return 0;
}
new),就必须同时正确实现析构、拷贝构造、拷贝赋值(三法则;C++11 起还有移动构造/移动赋值,称五法则),否则默认浅拷贝会让两个对象指向同一块内存,析构时双重释放。现代写法是用智能指针或 STL 容器替你管资源,根本不用手写这些(见第 13 节)。
: len(n) 初始化成员,比在函数体内 len = n; 赋值更高效;const 成员、引用成员、无默认构造的子类只能在初始化列表里初始化。
7. 继承公有继承、is-a、访问控制、构造顺序
#include <iostream>
using namespace std;
class Animal {
public:
void eat() { cout << "eating\n"; }
protected:
int legs = 4; // 子类可见,对外不可见
};
class Dog : public Animal { // 公有继承 = is-a
public:
void bark() { cout << "wang, legs=" << legs << "\n"; }
};
int main() {
Dog d;
d.eat(); // 继承自 Animal
d.bark();
return 0;
}
| 继承方式 | 基类 public | 基类 protected | 基类 private |
|---|---|---|---|
| public | public | protected | 不可见 |
| protected | protected | protected | 不可见 |
| private | private | private | 不可见 |
public 继承表达“是一个”关系。派生类构造时先构造基类再构造自己,析构顺序相反。多态基类的析构必须 virtual(见下节)。
8. 多态与虚函数虚函数表、虚析构、纯虚/抽象类、override
用基类指针/引用指向派生类对象,调用虚函数时,运行时按对象“真实类型”决定调用哪个版本——这就是运行时多态。
#include <iostream>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // 纯虚函数 → 抽象类,不能实例化
virtual ~Shape() = default; // 多态基类:析构必须 virtual
};
class Circle : public Shape {
double r;
public:
Circle(double r) : r(r) {}
double area() const override { return 3.14159 * r * r; }
};
int main() {
Shape* s = new Circle(2.0);
cout << s->area() << "\n"; // 12.56... 调的是 Circle::area
delete s; // 虚析构保证调用 Circle 析构
return 0;
}
delete 派生类对象时,基类析构必须为 virtual,否则只调用基类析构,派生部分内存泄漏。C++11 起用 override 关键字,让编译器检查你是否真的重写了虚函数(拼错函数名会直接报错而不是悄悄新建函数)。
9. 运算符重载成员/友元、常见重载、约定
#include <iostream>
using namespace std;
class Vec {
int x, y;
public:
Vec(int x, int y) : x(x), y(y) {}
Vec operator+(const Vec& o) const { return Vec(x + o.x, y + o.y); }
bool operator==(const Vec& o) const { return x == o.x && y == o.y; }
friend ostream& operator<<(ostream& os, const Vec& v) {
return os << "(" << v.x << "," << v.y << ")";
}
};
int main() {
Vec a(1, 2), b(3, 4);
cout << (a + b) << " " << (a == b) << "\n"; // (4,6) 0
return 0;
}
+ 写成减法)。二元运算符若需访问私有成员,常写为友元函数(friend)。赋值运算符 operator= 要注意自赋值与深拷贝(配合第 6 节)。
10. 模板函数模板、类模板、特化思路
模板让函数/类“参数化类型”,写一次适配多种类型,是泛型编程的基础。
#include <iostream>
using namespace std;
template <typename T>
T maxof(T a, T b) { return a > b ? a : b; }
template <typename T>
class Box {
T v;
public:
Box(T v) : v(v) {}
T get() const { return v; }
};
int main() {
cout << maxof(3, 7) << "\n"; // 7
cout << maxof(3.14, 2.7) << "\n"; // 3.14
Box<string> b("hi");
cout << b.get() << "\n"; // hi
return 0;
}
maxof<int> 就生成一份 int 版本,用到 Box<string> 就生成一份 string 版本。所以模板实现通常写在头文件里(编译器需要看到完整定义才能实例化)。
11. STL 容器与算法vector/map/set、迭代器、algorithm、范围 for
STL(标准模板库)是 C++ 生产力的核心,提供容器、迭代器、算法三者协作。
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {5, 2, 9, 1};
sort(v.begin(), v.end()); // 算法 + 迭代器
for (int x : v) cout << x << " "; // 范围 for:1 2 5 9
cout << "\n";
map<string, int> m; // 有序键值对(红黑树)
m["alice"] = 90;
m["bob"] = 80;
if (m.count("alice")) cout << m["alice"] << "\n";
auto it = find(v.begin(), v.end(), 5); // 算法 find
if (it != v.end()) cout << "found 5\n";
return 0;
}
| 容器 | 特点 | 典型场景 |
|---|---|---|
vector | 动态数组,尾部高效 | 通用序列 |
string | 字符序列 | 文本 |
map / set | 红黑树,有序 | 需要有序/去重 |
unordered_map | 哈希,平均 O(1) | 高频查找 |
stack / queue | 适配器 | 算法结构 |
vector 而非手写数组/链表;用 algorithm 里的 sort/find/accumulate/count 而非手搓循环。
12. new/delete 与 RAII配对、数组形式、资源获取即初始化
C++ 用 new/delete 代替 malloc/free,会调用构造/析构。更关键的是 RAII(Resource Acquisition Is Initialization):把资源绑定到对象生命周期,对象离开作用域时析构自动释放。
#include <iostream>
using namespace std;
int main() {
int* p = new int(42);
cout << *p << "\n";
delete p; // 配 new(不是 free)
p = nullptr;
int* arr = new int[10]; // 数组用 delete[]
delete[] arr;
return 0;
}
new 后忘了 delete 会泄漏;异常可能在 delete 前跳出导致泄漏。几乎永远不要裸写 new/delete——用智能指针(下节)或 STL 容器,让析构替你兜底。
std::lock_guard 锁、std::fstream 文件、std::unique_ptr 指针都是 RAII:构造即获得资源,析构即释放,异常安全。
13. 智能指针unique_ptr、shared_ptr、weak_ptr、make_*
智能指针是 RAII 的现成工具,自动管理生命周期,杜绝绝大多数内存问题。
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> u = make_unique<int>(10); // 独占所有权
cout << *u << "\n"; // 离开作用域自动 delete
shared_ptr<int> s1 = make_shared<int>(20); // 引用计数共享
shared_ptr<int> s2 = s1; // 计数 +1
cout << s1.use_count() << "\n"; // 2
return 0; // 计数归零自动释放
}
| 指针 | 语义 | 选用场景 |
|---|---|---|
unique_ptr | 独占,零开销 | 默认首选(大多数情况) |
shared_ptr | 引用计数共享 | 需要多处共享所有权 |
weak_ptr | 不增计数,观察 | 打破 shared_ptr 循环引用 |
make_unique / make_shared(异常安全、代码更短);传参时若只是借用用 const shared_ptr& 或原始指针 T*,不要无脑按值传 shared_ptr(会无谓增计数)。
14. 异常处理throw、try/catch、标准异常、栈展开
#include <iostream>
#include <stdexcept>
using namespace std;
int divide(int a, int b) {
if (b == 0) throw runtime_error("div by zero");
return a / b;
}
int main() {
try {
cout << divide(10, 0) << "\n";
} catch (const exception& e) { // 按基类引用捕获
cerr << "error: " << e.what() << "\n";
}
return 0;
}
std::runtime_error、std::invalid_argument 等);按 const exception& 捕获基类以便处理所有派生;异常沿调用栈“栈展开”,局部对象会正常析构(RAII 保证资源不漏)。
15. Lambda 表达式捕获列表、参数、mutable、配合算法
C++11 的 Lambda 是“匿名函数对象”,配合 STL 算法极好用。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> v = {5, 2, 9, 1, 7};
sort(v.begin(), v.end(), [](int a, int b) { return a > b; }); // 降序
for (int x : v) cout << x << " ";
cout << "\n";
int base = 3;
auto f = [base](int x) { return x + base; }; // 值捕获 base
cout << f(10) << "\n"; // 13
return 0;
}
| 捕获 | 含义 |
|---|---|
[] | 不捕获 |
[x] | 值捕获 x |
[&x] | 引用捕获 x |
[=] | 值捕获所有 |
[&] | 引用捕获所有 |
[&] 引用捕获或按需 [x, &y] 精确捕获,避免 [=] 不小心拷贝大对象;引用捕获要留意生命周期(别捕获局部变量返回出去)。
16. 移动语义与 auto右值引用、std::move、auto、范围 for
C++11 引入右值引用 && 与 std::move,把“即将销毁的临时对象”的资源“搬家”而非深拷贝,大幅提升性能。
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
int main() {
vector<int> a = {1, 2, 3};
vector<int> b = move(a); // 把 a 的资源移动给 b,a 变为空(可析构状态)
cout << "b=" << b.size() << " a=" << a.size() << "\n";
auto it = b.begin(); // auto 自动推导类型
for (auto& x : b) x *= 2; // 范围 for + 引用修改
return 0;
}
auto 减少冗长类型书写、避免写错类型(如复杂迭代器 vector<int>::const_iterator),遍历容器用 for (auto& x : v) 既简洁又高效。
17. C/C++ 差异与陷阱移植注意、常见错误对照
- C++ 类型检查更严格:
void*不能隐式转其它指针,须显式强转。 malloc不调用构造、free不调用析构,混用危险;C++ 用new/delete或容器。- 用
nullptr而非NULL(类型安全,不会误当整数)。 - 定义变量不必集中在函数开头,可在使用时定义(RAII 友好)。
struct在 C++ 里默认 public,且可直接当类型名用(不必写struct X)。- 默认全局
const变量在 C++ 是内部链接(C 是外部),跨文件要用extern const。
进阶训练场(含答案)
题 1:写一个带构造/析构的计数器类
类 Counter 持有一个 int 计数,构造时打印 "ctor" 并初始化为 0,inc() 自增,get() 返回,析构打印 "dtor"。
#include <iostream>
using namespace std;
class Counter {
int n = 0;
public:
Counter() { cout << "ctor\n"; }
~Counter() { cout << "dtor\n"; }
void inc() { n++; }
int get() const { return n; }
};
题 2:用 vector + lambda 求偶数平方和
给定 vector<int>,用 accumulate 与 lambda 求所有偶数的平方之和。
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
int sum_even_sq(const vector<int>& v) {
return accumulate(v.begin(), v.end(), 0,
[](int acc, int x) { return x % 2 == 0 ? acc + x * x : acc; });
}
题 3:用 unique_ptr 管理动态对象
用 make_unique 创建 int,读值后离开作用域,验证无内存泄漏(无需手写 delete)。
#include <iostream>
#include <memory>
using namespace std;
int main() {
auto p = make_unique<int>(99);
cout << *p << "\n"; // 99,离开作用域自动释放
return 0;
}
题 4:抽象基类 Shape + 两个派生类
Shape 有纯虚 area(),派生 Rect(w,h) 与 Circle(r),用基类指针(智能指针)遍历打印面积。
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
struct Shape { virtual double area() const = 0; virtual ~Shape() = default; };
struct Rect : Shape { double w, h; Rect(double w,double h):w(w),h(h){}
double area() const override { return w * h; } };
struct Circle : Shape { double r; Circle(double r):r(r){}
double area() const override { return 3.14159 * r * r; } };
int main() {
vector<unique_ptr<Shape>> v;
v.push_back(make_unique<Rect>(3, 4));
v.push_back(make_unique<Circle>(2));
for (auto& s : v) cout << s->area() << "\n";
return 0;
}
题 5:函数模板实现通用 min
写模板函数 my_min,支持 int、double、string 等可比较类型,返回较小者的 const 引用。
template <typename T>
const T& my_min(const T& a, const T& b) {
return (a < b) ? a : b;
}
// my_min(3,5) → 3;my_min(string("b"),string("a")) → "a"
题 6:用 map 统计单词出现次数
给定字符串数组,统计每个单词出现次数并输出。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
string words[] = {"a", "b", "a", "c", "b", "a"};
map<string, int> cnt;
for (auto& w : words) cnt[w]++;
for (auto& kv : cnt) cout << kv.first << ":" << kv.second << "\n";
return 0;
}
题 7:自定义类支持排序(运算符重载)
定义 Person(name, age),重载 < 使 vector<Person> 按 age 升序排。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Person {
string name; int age;
bool operator<(const Person& o) const { return age < o.age; }
};
int main() {
vector<Person> v = {{"A",30},{"B",20},{"C",25}};
sort(v.begin(), v.end());
for (auto& p : v) cout << p.name << " " << p.age << "\n";
return 0;
}
题 8:try/catch 包装除法
写函数对除零抛异常,main 里捕获并打印错误信息,验证程序不崩溃。
#include <iostream>
#include <stdexcept>
using namespace std;
double safe_div(double a, double b) {
if (b == 0) throw runtime_error("divide by zero");
return a / b;
}
int main() {
try { cout << safe_div(1, 0) << "\n"; }
catch (const exception& e) { cerr << "err: " << e.what() << "\n"; }
return 0;
}
💡 连老师都不会教你的干货(工业级技巧)
- 对象模型。对象在内存里就是"数据 + 布局",成员函数不占对象空间(存在于代码段),靠隐式
this区分实例;虚函数通过对象里的 vptr 指向 vtable 实现多态,每个含虚函数的对象因此多一个指针开销。 - 零开销抽象(Zero-overhead)。模板/内联/RAII 在 release 下不引入运行时成本——你不为不用的东西付代价。这是 C++ 的设计哲学,也区别于带虚拟机的语言。
- 值语义 vs 引用语义。C++ 默认值语义(拷贝即独立副本),与 Java/Python"引用即共享"完全不同。搞懂这点才明白拷贝构造、移动语义为何关键。
- 三/五法则。类自己管理资源(自己 new)时,必须同时正确实现析构、拷贝构造、拷贝赋值;C++11 起还要移动构造、移动赋值(五法则)。缺任何一个都会浅拷贝 → 双重释放。
- 移动语义与 std::move。把"即将销毁的对象"的资源"偷"过来,避免昂贵深拷贝。但
std::move后原对象处于"有效但未指定状态",不可再使用其值。 - RAII 是资源管理的核心。把资源(内存/文件/锁)绑定到对象生命周期:构造获取、析构释放,异常安全且不会漏。智能指针、
lock_guard都是 RAII。 - 不要裸 new/delete。优先
unique_ptr(独占)/shared_ptr(共享)/ 容器 vector。裸指针只做"观察",不拥有资源。 - 虚析构函数。基类若会被多态删除(基类指针删派生对象),析构必须
virtual,否则派生部分不释放,造成内存泄漏。 - 初始化顺序陷阱。基类先于成员构造;成员按声明顺序(不是初始化列表顺序)构造;跨编译单元的全局对象初始化顺序未定义(Static Initialization Order Fiasco),别在全局构造里依赖其他全局。
- 模板两阶段编译。模板定义时只检查不依赖模板参数的部分,实例化时才检查依赖部分;报错常出现在调用点,要学会读冗长报错。
- 名字查找(ADL)。函数调用时编译器会到参数所在命名空间找候选,使
std::swap等被"自动"找到,也偶尔引发意外重载,理解它才能看懂某些诡异调用。 - ABI 稳定性。C++ 没有官方稳定的 ABI,不同编译器/版本混链可能崩溃;而 C 的 ABI 长期稳定——这就是为什么系统接口、嵌入式多为 C。
- const 正确性与 mutable。const 成员函数承诺不改对象逻辑状态;需要缓存的字段可用
mutable在 const 方法里修改。 - 异常开销与 noexcept。异常路径有成本;标明
noexcept让编译器更激进优化,且对移动操作至关重要(否则回退到拷贝)。
自测题(点击展开答案)
- Q1:C++ 中传参想“改外部变量且避免拷贝”,应优先用?A:引用
T&(只读时用const T&),比指针安全、比值传递高效。 - Q2:基类指针删除派生类对象时,基类析构应声明为?A:
virtual。否则只调基类析构,派生部分泄漏。 - Q3:现代 C++ 管理动态内存首选?A:智能指针(unique_ptr/shared_ptr)或 STL 容器,几乎不要裸 new/delete。
- Q4:空指针字面量应写?A:
nullptr(类型安全),而非NULL(本质是 0)或0。 - Q5:
auto与范围 for 主要带来什么好处?A:减少冗长类型书写、避免类型写错,让遍历容器更简洁安全。 - Q6:类手动管理资源(自己 new)时,必须遵守?A:三/五法则——同时正确实现析构、拷贝构造、拷贝赋值(及移动构造/赋值),否则浅拷贝导致双重释放。
- Q7:
shared_ptr循环引用应如何打破?A:把其中一方改为weak_ptr,它不增加引用计数,避免两者互相持有导致永不释放。 - Q8:C++ 里
using namespace std;写在头文件有何风险?A:会把 std 全部名字暴露到所有包含该头文件的翻译单元,易引发命名冲突;头文件里应避免。
🧪 动手实验室
不用从零敲代码:下面每个程序都已写好,点「▶ 运行」直接看结果;想改就在框里改,再点运行。手机、电脑都一样用。打开本页时会自动跑一次,方便你先看效果。
① 用 iostream 输出(对比 C 的 printf)
② 引用做交换(C++ 特色,不用指针)
③ 用 vector + 范围 for(比数组更现代)
④ 简单类与对象